本文主要是我在看《疯狂Java讲义》时的读书笔记,阅读的比较仓促,就用 markdown 写了个概要。
第七章 Java集合(重点)
1、 Java集合概述
Java集合类是一种工具类,主要用来存储数量不定的对象,类似于容器。Java主要有四大集合体系:Set
、 List
、Queue
、Map
。
所有的集合类都位于java.util
包下,后来为了支持多线程又在java.util.concurrent
包下提供了一些线程安全的集合类(本章不讨论)。
Java集合类主要派生自两个接口类:Collection 和 Map,继承树如下图:
图中粗边框的Set、List、Queue、Map仍然是接口,而以灰色覆盖的是常用的实现类,比如HashSet
、TreeSet
、ArrayList
、LinkedList
、ArrayDeque
、HashMap
、TreeMap
等。
2、如何遍历集合
Iterator接口遍历集合
Iterator
迭代器提供了遍历Collection集合元素的统一编程接口,它定义了几个方法:
boolean hasNext()
:集合元素是否被遍历完Object next()
:返回集合里的下一个元素void remove()
:删除集合里上一次next方法返回的元素void forEachRemaining(Consumer action)
:Java 8新增方法,用于Lambda表达式遍历集合。
1 | import java.util.HashSet; |
Lambda表达式遍历集合
Java 8支持lambda表达式,并且为每个可迭代的集合类新增了一个forEach(Consumer action)
方法,参数类型是一个函数式接口。1
2
3
4
5
6
7
8
9
10
11import java.util.HashSet;
public class MyClass {
public static void main(String[] args) {
HashSet<String> set = new HashSet<String>();
set.add("Firstly");
set.add("Secondly");
set.add("Thirdly");
set.forEach(obj -> System.out.println(obj)); // Lambda表达式
}
}
foreach循环遍历集合
类似于C++11中的范围循环1
2
3
4
5
6
7
8
9
10
11
12
13import java.util.HashSet;
public class MyClass {
public static void main(String[] args) {
HashSet<String> set = new HashSet<String>();
set.add("Firstly");
set.add("Secondly");
set.add("Thirdly");
for(Object obj : set) {
System.out.println(obj);
}
}
}
3、Set集合
HashSet类
- 采用Hash算法来存储集合中的元素(由hashCode值决定存储位置),故拥有很好的存取和查找性能。
- 不允许包含相同的元素,相同的标准是“通过
equals()
比较相等且hashCode()
返回值也相等” - 不能保证元素的排列顺序,顺序可能与添加顺序不同。
- 集合元素值可以是
null
。
TreeSet类
- 采用红黑树来存储集合中的元素。
- 不允许包含相同的元素,相同的标准是“通过
compareTo(Object obj)
比较返回0” - 集合中的元素是有序的,默认使用
compareTo
升序排列,当然你也可以通过Comparator
对象自定义排序规则。 - 集合元素不可以是
null
。
EnumSet类
- 采用位向量的形式存储元素,紧凑高校,运行效率高。
- 集合中的多个枚举值必须属于同一个枚举类
- 各元素按Enum类内的定义顺序有序
- 集合元素不可以是
null
。
结论:上述Set的三个实现类都不是线性安全的。HashSet
和TreeSet
作为Set类的两个典型实现,前者的性能总是比后者好,因为后者需要额外维护一棵红黑树,但后者是有序的,所以需要根据具体需求来选择。
4、Queue集合
Queue用于模拟“先进先出”队列,不允许随机访问。
PriorityQueue类
- 不同于FIFO队列,PriorityQueue是按优先权排列,默认就是按元素大小进行排列。
- 本质上是一个最小堆。
- 不允许插入null元素
Deque接口与ArrayDeque类
- Deque是Queue的子接口,它代表一个双端队列。而
ArrayDeque
类是Deque的一个典型实现。 - ArrayDeque类是基于数组实现的。
- ArrayDeque类是一个双端队列,所以即可以作为队列使用,也可以作为栈使用。
5、List集合
ArrayList类与Vector类
- ArrayList类和Vector类都是基于数组实现的(
Object[]
); - ArrayList类和Vector类在用法上完全相同,但Vector是一个古老的类,有很多缺点,尽量少用;
- ArrayList类和Vector类的显著区别:
ArrayList
是线程不安全的,Vector
是线程安全的。
LinkedList类
- 同时实现了List接口与Deque接口,所以可以作为List集合、双端队列、栈使用。
- LinkedList类是基于链表实现的,插入/删除性能好。
6、Map集合
HashMap类与Hashtable类
- HashMap与Hashtable的关系完全类似于ArrayList和Vector,
Hashtable
太古老,尽量少用;- HashMap线程不安全,Hashtable线程安全
- HashMap可以插入null作为key/value,但Hashtable不可以
- HashMap/Hashtable不能保证元素的顺序,因为它们的key保存方式与HashSet完全相同。
- HashMap/Hashtable判断两个key相等的标准:两个key通过
equals()
比较返回true时,它们的hashCode()
也相等。
TreeMap类
- 基于红黑树实现,每个kv对就是红黑树的一个节点。
- 类似于
TreeSet
类,TreeMap
类根据key保持有序。默认使用 compareTo() 升序排列,当然你也可以通过 Comparator 对象自定义排序规则。 - 不允许包含相同的元素,相同的标准是“通过
compareTo(Object obj)
比较返回0”
EnumMap类
- 类似于
EnumSet
类,EnumMap
类的key必须是同一个枚举类的枚举值 - 根据 key 有序(枚举类中定义的顺序)
- 不允许null作为key,但允许null作为value。
7、什么是rehash?
所谓的rehash
,是指当hash表中的槽位被填满到一定程度(最大负载因子)时,hash表会自动成倍地增加容量,并将原来的对象重新分配,hash到新的表中。
最大负载因子,即负载极限。HashSet、HashMap、Hashtable的默认负载极限是0.75
,这是时间和空间上的一种折中,在保证查询性能的同时,尽量减少哈希表的内存开销。
8、操作集合的工具类Collections
Java提供了一个操作Set、List和Map等集合的工具类Collections
,该工具类里提供了大量方法对集合元素进行排序、查找和修改……另外,还可以对集合对象实现同步控制以及将集合对象设置为不可变。
线程同步控制
上面介绍的集合类中,除了古老的Vector和Hashtable之外,都是线程不安全的。Collections工具类提供了多个synchronizedXxx()
方法,用于将指定集合包装成线程安全的集合:1
2
3
4
5
6
7
8
9import java.util.*;
public class MyClass {
public static void main(String[] args) {
List l = Collections.synchronizedList(new ArrayList());
Set s = Collections.synchronizedSet(new HashSet());
Map m = Collections.synchronizedMap(new HashMap());
}
}
设置不可变集合
Collections提供了三个方法来返回一个不可变的集合:
emptyXxx()
:返回一个空的、不可变的集合对象。singletonXxx()
:返回一个只有一个元素、且不可改变的集合对象。unmodifiableXxx()
:返回指定集合对象的不可变版本(只读)。
第八章 泛型
1、泛型的概念
在Java没有泛型之前,一旦把一个对象“丢进”Java集合中,集合就会忘记对象的类型,把所有对象当成 Object 类型处理。当从集合中取出对象后,就需要进行强制类型转换。这种强制类型转换不仅使代码臃肿,而且容易引起ClassCastException
异常。下面就是一个例子:1
2
3
4
5
6
7
8
9
10
11
12import java.util.ArrayList;
public class MyClass {
public static void main(String[] args) {
ArrayList strs = new ArrayList();
strs.add("one");
strs.add("two");
strs.add(3); // 把一个Integer对象丢进了集合
// 遍历输出时报ClassCastException异常
strs.forEach(str -> System.out.println(((String)str).length()));
}
}
从Java 5以后,Java引入了“参数化类型”的概念,允许程序在创建集合时指定集合元素的类型。注意,如果没有传入类型实参,那么类型参数 T 将会当成Object
类型处理。1
2ArrayList<String> strs = new ArrayList<String>();
ArrayList<String> strs = new ArrayList<>(); // 后面可以只带尖括号
这种参数化类型就称为泛型(Generic)
2、泛型的好处
- 增加泛型支持之后,集合完全可以记住元素的类型,并可以在编译时检查添加元素是否满足类型要求,不满足的话,编译器会提示错误;
- 泛型可以减少强制类型转换,使代码更加简洁;
- 泛型可以保证如果程序在编译时没有发出警告,运行时就不会产生
ClassCastException
异常,使程序更加健壮。
3、自定义泛型类
相信学过《C++函数模板与类模板》的,对Java的泛型编程并不难理解,这里就不赘述了。示例:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19public class People<T> {
private T info;
public People(){}
public People(T info) {
this.info = info;
}
public T getInfo() {
return this.info;
}
public static void main(String[] args) {
People<String> he = new People<>("James Bond");
People<Integer> she = new People<>(25);
System.out.println("his name is "+he.getInfo()+", her age is "+she.getInfo());
}
}
另外,Java中还可以对类型参数 T 进行限制:1
2
3public class People<T extends Number> {
//......
}
这时传入的类型实参 必须是Number类或它的子类。
4、类型通配符
将一个问号作为 类型实参 传给支持泛型的集合,比如List<?>
,它的元素类型可以匹配任何类型,这个问号?
被称为通配符。
1 | import java.util.*; |
设定通配符的上限:
List<?>
表示匹配所有的类型。但是有时候我们希望只匹配一部分,比如只匹配某个类以及它的子类,就可以这样写:1
List<? extends People>
这里的问号?
仍代表一个未知类型,但这个未知类型必须是People类或它的子类。
设定通配符的下限:1
List<? super People>
这里的问号?
仍代表一个未知类型,但这个未知类型必须是People类或它的父类。
5、自定义泛型方法
语法如下:1
2
3
4修饰符 <T, S> 返回值类型 函数名(形参列表)
{
// 函数体
}
例如下面这个泛型方法:1
2
3
4
5static <T> void myFunction(Collection<T> arr, Collection<T> c) {
for(T elem : arr) {
c.add(elem);
}
}
上述泛型方法可以直接调用,编译器会根据实参推断类型参数的值。但其实,也可以直接用类型通配符替代(两个Collection的元素类型没有依赖关系的情况下):1
2
3
4
5static void myFunction(Collection<?> arr, Collection<?> c) {
for(T elem : arr) {
c.add(elem);
}
}
第九章 异常处理
1、Java异常的继承体系
如上图所示,java.lang.Throwable
是Java中所有可以错误和异常的父类。Java把所有的非正常情况分为两类:
Error(错误):一般指与虚拟机相关的问题,如系统崩溃、虚拟机错误、动态链接失败等,应用程序无法处理这些错误。
Exception(异常):指应用程序本身可以处理的异常。
- 运行时异常:指
RuntimeException
类及其子类异常,编译器不会检查这些异常。 - 非运行时异常:也叫Checked异常,是指
RuntimeException
以外的 Exception。这些异常不处理,程序就不能编译通过。如IOException
、SQLException
等
- 运行时异常:指
2、异常处理机制
Java的异常机制主要依赖于try
、catch
、finally
、throw
和throws
五个关键字。
捕获异常:try-catch语句
1 | try { |
如果找不到能捕获该异常的catch块,则运行时环境终止,Java程序也将退出。
捕获异常:try-catch-finally语句
try-catch 语句还可以包括第三部分,就是finally块。无论是否出现异常,finally块总会被执行;甚至在try块或catch块中return了,finally块也会被执行。(除非System.exit()
)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15try {
// 业务实现代码
......
} catch (ExceptionClass1 e1) {
// 异常处理块1
......
} catch (ExceptionClass2 e2) {
// 异常处理块2
......
}
......
finally {
// 资源回收块
......
}
finally块通常用来回收在try块里打开的一些物理资源,例如数据库连接、网络连接、磁盘文件等。
抛出异常:throws
throws
关键字只能用在方法签名中。如果当前函数不知道如何处理这种异常,则应该使用 throws 抛出异常,交由上一级调用者处理 —— 若main
方法 throws 异常,该异常将交给JVM处理(打印跟踪栈信息并终止程序运行)。1
public static void main(String[] args) throws IOException
抛出异常:throw
当程序出现错误时,系统会自动抛出异常;除此之外,程序也可以自己抛出异常。throw
单独作为语句使用,用于抛出一个具体的异常对象。1
throw new Exception("您输入的格式有误!");
3、打印异常信息
当在catch块中捕获了异常,你可能想要获取或打印异常对象的相关信息。所有的异常对象都包含几个常用方法:
getMessage()
:返回该异常的详细描述字符串。printStackTrace()
:将该异常的跟踪栈信息输出到标准错误输出。printStackTrace(PrintStream s)
:将该异常的跟踪栈信息输出到指定输出流。getStackTrace()
:返回该异常的跟踪栈信息。
4、自定义异常类
用户自定义异常都应该继承Exception
基类,当然如果希望自定义运行时异常,则应该继承RuntimeException
基类。
定义异常类时通常需要提供两个构造器:一个是无参数的构造器,另一个是带一个字符串参数的构造器(该字符串作为异常对象的描述信息)。1
2
3
4
5
6
7public class InputException extends Exception
{
public InputException(){}
public InputException(String msg) {
super(msg);
}
}
5、异常链
对于大型应用而言,通常有严格的分层关系,上层功能的实现依赖下层的API,如图:
如果当 中间层 访问 持久层出现SQLException异常时,程序不应该把底层的SQLException异常传到用户界面,因为:
- 用户并不想看到底层的SQLException异常,该异常对他们使用该系统没有任何帮助;
- 将底层异常暴露出来不安全。
所以通常的做法就是:程序先捕获原始异常,然后抛出一个新的业务异常。(新的业务异常中包含对用户的提示信息)1
2
3
4
5
6
7
8
9
10
11
12try {
// 业务逻辑代码
......
} catch (SQLException sqle) {
// 把原始异常记录下来,留给管理员
......
throw new UserException("访问数据库出现异常");
} catch (Exception e) {
//把原始异常记录下来,留给管理员
......
throw new UserException("系统出现未知异常");
}
这种捕获一个异常然后接着抛出另一个异常,并把原始异常信息保存下来是一种典型的链式处理,也被称作“异常链”。
从Java 4以后,所有Throwable
子类在构造器中都可以接收一个Exception对象,这样就可以很容易把原始异常作为参数传递给新的异常,创建并抛出新的异常。也能通过异常链追踪到异常最初发生的位置。示例如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19public class UserException extends Exception
{
public UserException(){}
public UserException(String msg) {
super(msg);
}
public UserException(Throwable t) { // 带Throwable参数的构造器
super(t);
}
}
/*---------------------------------------------*/
try {
// 业务逻辑代码
...
} catch (SQLException sqle) {
throw new UserException(sqle); // 封装原始异常
} catch (Exception e) {
throw new UserException(e); // 封装原始异常
}