Java基础笔记(二) 集合、泛型、异常处理

本文主要是我在看《疯狂Java讲义》时的读书笔记,阅读的比较仓促,就用 markdown 写了个概要。

第七章 Java集合(重点)

1、 Java集合概述

Java集合类是一种工具类,主要用来存储数量不定的对象,类似于容器。Java主要有四大集合体系:SetListQueueMap

所有的集合类都位于java.util包下,后来为了支持多线程又在java.util.concurrent包下提供了一些线程安全的集合类(本章不讨论)。

Java集合类主要派生自两个接口类:CollectionMap,继承树如下图:



图中粗边框的Set、List、Queue、Map仍然是接口,而以灰色覆盖的是常用的实现类,比如HashSetTreeSetArrayListLinkedListArrayDequeHashMapTreeMap等。

2、如何遍历集合

Iterator接口遍历集合

Iterator迭代器提供了遍历Collection集合元素的统一编程接口,它定义了几个方法:

  • boolean hasNext():集合元素是否被遍历完
  • Object next():返回集合里的下一个元素
  • void remove():删除集合里上一次next方法返回的元素
  • void forEachRemaining(Consumer action):Java 8新增方法,用于Lambda表达式遍历集合。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import java.util.HashSet;
import java.util.Iterator;

public class MyClass {
public static void main(String[] args) {
HashSet<String> set = new HashSet<String>();
set.add("Firstly");
set.add("Secondly");
set.add("Thirdly");

Iterator it = set.iterator();
while(it.hasNext()) {
System.out.println(it.next());
}
}
}

Lambda表达式遍历集合

Java 8支持lambda表达式,并且为每个可迭代的集合类新增了一个forEach(Consumer action)方法,参数类型是一个函数式接口。

1
2
3
4
5
6
7
8
9
10
11
import 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
13
import 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的三个实现类都不是线性安全的。HashSetTreeSet作为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
9
import 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
12
import 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
2
ArrayList<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
19
public 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
3
public class People<T extends Number> {
//......
}

这时传入的类型实参 必须是Number类或它的子类。


4、类型通配符

将一个问号作为 类型实参 传给支持泛型的集合,比如List<?>,它的元素类型可以匹配任何类型,这个问号被称为通配符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import java.util.*;

public class MyClass{
// 遍历元素类型未知的List集合
public void traverse(List<?> list) {
list.forEach(e -> System.out.println(e));
}

public static void main(String[] args) {
List<String> myList = new ArrayList<>();
myList.add("Kobe");
myList.add("LeBron");
MyClass myClass = new MyClass();
myClass.traverse(myList);
}
}

设定通配符的上限

List<?>表示匹配所有的类型。但是有时候我们希望只匹配一部分,比如只匹配某个类以及它的子类,就可以这样写:

1
List<? extends People>

这里的问号?仍代表一个未知类型,但这个未知类型必须是People类或它的子类。

设定通配符的下限

1
List<? super People>

这里的问号?仍代表一个未知类型,但这个未知类型必须是People类或它的父类。


5、自定义泛型方法

语法如下:

1
2
3
4
修饰符 <T, S> 返回值类型 函数名(形参列表)
{
// 函数体
}

例如下面这个泛型方法:

1
2
3
4
5
static <T> void myFunction(Collection<T> arr, Collection<T> c) {
for(T elem : arr) {
c.add(elem);
}
}

上述泛型方法可以直接调用,编译器会根据实参推断类型参数的值。但其实,也可以直接用类型通配符替代(两个Collection的元素类型没有依赖关系的情况下):

1
2
3
4
5
static 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。这些异常不处理,程序就不能编译通过。如IOExceptionSQLException

2、异常处理机制

Java的异常机制主要依赖于trycatchfinallythrowthrows五个关键字。

捕获异常:try-catch语句

1
2
3
4
5
6
7
8
9
10
11
12
try {
statement1
statement2 // 出现异常,系统生成异常对象ex
......
} catch (ExceptionClass1 e1) { // ex对象是否属于ExceptionClass1类及其子类
exception handler statement1
......
} catch (ExceptionClass2 e2) { // 一旦捕获,try-catch语句结束
exception handler statement
......
}
......

如果找不到能捕获该异常的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
15
try {
// 业务实现代码
......
} 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
7
public 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
12
try {
// 业务逻辑代码
......
} 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
19
public 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); // 封装原始异常
}