9.2.3 并不存在泛型类

前面提到可以把ArrayList<String>类当成ArrayList的子类,事实上, ArrayList<String>类也确实像种特殊的ArrayList类:该Arraylists<String>对象只能添加String对象作为集合元素。但实际上,系统并没有为ArrayList<String>生成新的class文件,而且也不会把ArrayList<String>当成新类来处理。

看下面代码的打印结果是什么?

1
2
3
4
5
//分别创建List<String>对象和List<Integer>对象
List<string> list1=new ArrayList<>();
List<Integer> list2 =new ArrayList<>();
//调用getClass()方法来比较list1和list2的类是否相等
System.out.printin(list1.getClass()==list2.getClass());

运行上面的代码片段,可能有读者认为应该输出false,但实际输出true。因为不管泛型的实际类型参数是什么,它们在运行时总有同样的类(class)。

静态方法 静态初始化块 静态变量的声明和初始化中不允许使用泛型形参

不管为泛型形参传入哪一种类型实参,对于Java来说,它们依然被当成同一个类处理,在内存中也只占用一块内存空间,因此在静态方法、静态初始化块或者静态变量的声明和初始化中不允许使用泛型形参。下面程序演示了这种错误。

1
2
3
4
5
6
7
8
9
10
public class R<T> {
// 下面代码错误,不能在静态变量声明中使用泛型形参
static T info;
// T age;

public void foo(T msg) {
}
// 下面代码错误,不能在静态方法声明中使用泛型形参
// public static void bar(T msg){}
}
1
Cannot make a static reference to the non-static type TJava(536871434)

9.2.2 从泛型类派生子类

当创建了带泛型声明的接口、父类之后,可以为该接口创建实现类,或从该父类派生子类,需要指出的是,当使用这些带泛型声明的接口、父类时不能再包含泛型形参例如,下面代码就是错误的。

1
2
3
//定义类A继承Appe类, Apple类不能跟泛型形参
public class A extends Apple<T>{
}

方法中的形参代表变量、常量、表达式等数据,本书把它们直接称为形参,或者称为数据形参

定义方法时可以声明数据形参,调用方法(使用方法)时必须为这些数据形参传入实际的数据;

与此类似的是,定义类、接口、方法时可以声明泛型形参,使用类、接口、方法时应该为泛型形参传入实际的类型

如果想从Apple类派生一个子类,则可以改为如下代码:

1
2
3
//使用 Apple类时为T形参传入String类型
public class A extends Apple<String>{
}

调用方法时必须为所有的数据形参传入参数值,与调用方法不同的是,使用类、接口时也可以不为泛型形参传入实际的类型参数,即下面代码也是正确的。

1
2
3
//使用 Apple类时,没有为T形参传入实际的类型参数
public class A extends Apple{
}

像这种使用Apple类时省略泛型的形式被称为原始类型(raw type)。

如果从Apple<String>类派生子类,则在Apple类中所有使用T类型的地方都将被替换成String类型,即它的子类将会继承到String getInfo()void setlnfo(String info)两个方法,如果子类需要重写父类的方法,就必须注意这一点。下面程序示范了这一点。

1
2
3
4
5
6
7
8
9
10
11
12
public class A1 extends Apple<String> {
// 正确重写了父类的方法,返回值
// 与父类Apple<String>的返回值完全相同
public String getInfo() {
return "子类" + super.getInfo();
}

// // 下面方法是错误的,重写父类方法时返回值类型不一致
// public Object getInfo() {
// return "子类";
// }
}

如果使用Apple类时没有传入实际的类型(即使用原始类型),java编译器可能发出警告:使用了未经检査或不安全的操作——这就是泛型检查的警告,读者在前一章中应该多次看到这样的警告。如果希望看到该警告提示的更详细信息,则可以通过为javac命令增加-Xlint unchecked选项来实现。此时,系统会把Apple<T类里的T形参当成Object类型处理。如下程序所示。

1
2
3
4
5
6
7
8
9
10
public class A2 extends Apple
{
// 重写父类的方法
public String getInfo()
{
// super.getInfo()方法返回值是Object类型,
// 所以加toString()才返回String类型
return super.getInfo().toString();
}
}

上面程序都是从带泛型声明的父类来派生子类,创建带泛型声明的接口的实现类与此几乎完全一样,此处不再赘述。

9.2 深入泛型

所谓泛型,就是允许在定义类、接口、方法时使用类型形参,这个类型形参(或叫泛型)将在声明变量、创建对象、调用方法时动态地指定(即传入实际的类型参数,也可称为类型实参)。

Java5改写了集合框架中的全部接口和类,为这些接口、类增加了泛型支持,从而可以在声明集合变量、创建集合对象时传入类型实参,这就是在前面程序中看到的List<String>ArrayList<String>两种类型。

9.2.1 定义泛型接口、类

下面是Java5改写后List接口、 Iterator接口、Map的代码片段。

1
2
3
4
5
6
7
8
9
......
// 定义接口时指定了一个泛型形参,该形参名为E
public interface List<E> extends Collection<E> {
......
// 在该接口里,E可作为类型,使用下面方法可以使用E作为参数类型
Iterator<E> iterator();
......
boolean add(E e); //1
.........
1
2
3
4
5
6
7
8
9
......
// 定义接口时指定了一个泛型形参,该形参名为E
public interface Iterator<E> {

// 在该接口里E完全可以作为类型使用
E next();
......
boolean hasNext();
}
1
2
3
4
5
6
7
8
9
......
public interface Map<K, V> {
......
// 在该接口里K、V完全可以作为类型使用
//2
V put(K key, V value);
......
Set<K> keySet();
......

上面三个接口声明是比较简单的,除了尖括号中的内容—这就是泛型的实质:
允许在定义接口、类时声明泛型形参,泛型形参在整个接口、类体内可当成类型使用,几乎所有可使用普通类型的地方都可以使用这种泛型形参

除此之外,1、2处方法声明返回值类型是Iterator<E>Set<K>,这表明Set<K>形式是一种特殊的数据类型,是一种与Set不同的数据类型:可以认为Set<K>Set类型的子类。

例如使用List类型时,如果为E形参传入String类型实参,则产生了一个新的类型: List<String>类型,可以把List<String>想象成E被全部替换成String的特殊List子接口。

1
2
3
4
5
6
//List<string>等同于如下接口
public interface ListString extends List
//原来的E形参全部变成String类型实参
void add(String x);
Iteratoer<String> iterator();
}

通过这种方式,就解决了9.1.2节中的问题,虽然程序只定义了一个List<E>接口,但实际使用时可以产生无数多个List接口,只要为E传入不同的类型实参,系统就会多出一个新的List子接口。不过必须指出:List<String>绝不会被替换成ListString,系统没有进行源代码复制,二进制代码中没有,磁盘中没有,内存中也没有。

包含泛型声明的类型可以在定义变量、创建对象时传入一个类型实参,从而可以动态地生成无数多个逻辑上的子类,但这种子类在物理上并不存在。

任何类和接口都可以加上泛型声明

可以为任何类、接口增加泛型声明(并不是只有集合类才可以使用泛型声明,虽然集合类是泛型的重要使用场所)。

程序示例

下面自定义一个Apple类,这个Apple类就可以包含一个泛型声明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 定义Apple类时使用了泛型声明
public class Apple<T> {
// 使用T类型定义实例变量
private T info;

public Apple() {
}

// 下面方法中使用T类型来定义构造器
public Apple(T info) {
this.info = info;
}

public void setInfo(T info) {
this.info = info;
}

public T getInfo() {
return this.info;
}

public static void main(String[] args) {
// 由于传给T形参的是String,所以构造器参数只能是String
Apple<String> a1 = new Apple<>("苹果");
System.out.println(a1.getInfo());
// 由于传给T形参的是Double,所以构造器参数只能是Double或double
Apple<Double> a2 = new Apple<>(5.67);
System.out.println(a2.getInfo());
}
}

上面程序定义了一个带泛型声明的Apple<T>类(不要理会这个泛型形参是否具有实际意义),使用Apple<T>类时就可为T形参传入实际类型,这样就可以生成如Apple<String>Apple<double>…形式的多个逻辑子类(物理上并不存在)。这就是9节可以使用List<String>ArrayList<String>等类型的原因—JDK在定义ListArray List等接口、类时使用了泛型声明,所以在使用这些类时为之传入了实际的类型参数。

泛型的构造器还是和类名相同

当创建带泛型声明的自定义类,为该类定义构造器时,构造器名还是原来的类名,不要增加泛型声明。例如,为Apple<T>类定义构造器,其构造器名依然是Apple,而不是Apple<T>!调用该构造器时却可以使用Apple<T>的形式,当然应该为T形参传入实际的类型参数。

Java7提供了菱形语法,允许省略<>中的类型实参。

9.1 泛型入门

Java集合有个缺点——把一个对象“丢进”集合里之后,集合就会“忘记”这个对象的数据类型,当再次取出该对象时,该对象的编译类型就变成了Object类型(其运行时类型没变)。

Java集合之所以被设计成这样,是因为集合的设计者不知道我们会用集合来保存什么类型的对象,所以他们把集合设计成能保存任何类型的对象,只要求具有很好的通用性。但这样做带来如下两个问题:

  1. 集合对元素类型没有任何限制,这样可能引发一些问题。例如,想创建一个只能保存Dog对象的集合,但程序也可以轻易地将Cat对象“丢”进去,所以可能引发异常。
  2. 由于把对象“丢进”集合时,集合丢失了对象的状态信息,集合只知道它盛装的是Object,因此取出集合元素后通常还需要进行强制类型转换。这种强制类型转换既增加了编程的复杂度,也可能引发ClassCastException异常。

下面将深入介绍编译时不检查类型可能引发的异常,以及如何做到在编译时进行类型检查。

9.1.1 编译时不检查类型的异常

下面程序将会看到编译时不检查类型所导致的异常。

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

public class ListErr {
public static void main(String[] args) {
// 创建一个只想保存字符串的List集合
List strList = new ArrayList();
strList.add("疯狂Java讲义");
strList.add("疯狂Android讲义");
// “不小心”把一个Integer对象“丢进”了集合
strList.add(5); // 1
strList.forEach(str -> System.out.println(((String) str).length())); // 2
}
}

上面程序创建了一个List集合,而且只希望该List集合保存字符串对象—但程序不能进行任何限制,如果程序在代码1处“不小心”把一个Integer对象“丢进”了List集合中,这将导致程序在代码2处引发ClassCastException异常,因为程序试图把一个Integer对象转换为String类型。

1
2
3
4
5
6
8
11
Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
at ListErr.lambda$0(ListErr.java:11)
at java.util.ArrayList.forEach(ArrayList.java:1257)
at ListErr.main(ListErr.java:11)

9.1.2 使用泛型

Java5以后,Java引入了“参数化类型( parameterized type)”的概念,允许程序在创建集合时指定集合元素的类型,正如在第8章的ShowHand.java程序中见到的List<String>,这表明该List只能保存字符串类型的对象。Java参数化类型被称为泛型(Generic)。

对于前面的ListErr.java程序,可以使用泛型改进这个程序。

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

public class GenericList {
public static void main(String[] args) {
// 创建一个只想保存字符串的List集合
List<String> strList = new ArrayList<String>(); // 1
strList.add("疯狂Java讲义");
strList.add("疯狂Android讲义");
// 下面代码将引起编译错误
// strList.add(5); // 2
strList.forEach(str -> System.out.println(str.length())); // 3
}
}

上面程序成功创建了一个特殊的List集合:strList,这个List集合只能保存字符串对象,不能保存其他类型的对象。创建这种特殊集合的方法是:在集合接口、类后增加尖括号,尖括号里放一个数据类型,即表明这个集合接口、集合类只能保存特定类型的对象。注意代码1处的类型声明,它指定strList不是一个任意的List,而是一个String类型的List,写作:List<String>。可以称List是带一个类型参数的泛型接口,在本例中,类型参数是String。在创建这个ArrayList对象时也指定了一个类型参数。

上面程序将在代码2处引发编译异常,因为strList集合只能添加String对象,所以不能将Integer对象“丢进”该集合。

而且程序在代码3处不需要进行强制类型转换,因为strList对象可以“记住”它的所有集合元素都是String类型。

上面代码不仅更加健壮,程序再也不能“不小心”地把其他对象“丢进” strList集合中;而且程序更加简洁,集合自动记住所有集合元素的数据类型,从而无须对集合元素进行强制类型转换。这一切,都是因为Java5提供的泛型支持

9.1.3 Java9增强的“菱形”语法

Java7以前,如果使用带泛型的接口、类定义变量,那么调用构造器创建对象时构造器的后面也必须带泛型,这显得有些多余了。例如如下两条语句:

1
2
List<String> strList new ArrayList<String>();
Map<String Integer> scores=new HashMap<String,Integer>();

上面两条语句中的构造器后面的泛型信息(<String,Integer>)完全是多余的,在Java7以前这是必需的,不能省略。

Java7开始,Java允许在构造器后不需要带完整的泛型信息,只要给出一对尖括号(<>)即可,Java可以推断尖括号里应该是什么泛型信息。即上面两条语句可以改写为如下形式:

1
2
List<String> strList new ArrayList<>();
Map<String Integer> scores=new HashMap<>();

把两个尖括号并排放在一起非常像一个菱形,这种语法也就被称为菱形语法。下面程序示范了Java7的菱形语法。

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

public class DiamondTest {
public static void main(String[] args) {
// Java自动推断出ArrayList的<>里应该是String
List<String> books = new ArrayList<>();// 粗字体代码
books.add("疯狂Java讲义");
books.add("疯狂Android讲义");
// 遍历books集合,集合元素就是String类型
books.forEach(ele -> System.out.println(ele.length()));
// Java自动推断出HashMap的<>里应该是String , List<String>
Map<String, List<String>> schoolsInfo = new HashMap<>(); // 粗字体代码
// Java自动推断出ArrayList的<>里应该是String
List<String> schools = new ArrayList<>(); // 粗字体代码
schools.add("斜月三星洞");
schools.add("西天取经路");
schoolsInfo.put("孙悟空", schools);
// 遍历Map时,Map的key是String类型,value是List<String>类型
schoolsInfo.forEach((key, value) -> System.out.println(key + "-->" + value));
}
}

上面程序中三行粗体字代码就是“菱形”语法的示例。从该程序不难看出,“菱形”语法对原有的泛型并没有改变,只是更好地简化了泛型编程

Java9再次增强了菱形语法,它甚至允许在创建匿名内部类时使用菱形语法,Java可根据上下文来推断匿名内部类中泛型的类型。下面程序示范了在匿名内部类中使用菱形语法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
interface Foo<T> {
void test(T t);
}

public class AnnoymousDiamond {
public static void main(String[] args) {
// 指定Foo类中泛型为String
Foo<String> f = new Foo<>() {
// test()方法的参数类型为String
public void test(String t) {
System.out.println("test方法的t参数为:" + t);
}
};
// 使用泛型通配符,此时相当于通配符的上限为Object
Foo<?> fo = new Foo<>() {
// test()方法的参数类型为Object
public void test(Object t) {
System.out.println("test方法的Object参数为:" + t);
}
};
// 使用泛型通配符,通配符的上限为Number
Foo<? extends Number> fn = new Foo<>() {
// 此时test()方法的参数类型为Number
public void test(Number t) {
System.out.println("test方法的Number参数为:" + t);
}
};
}
}

上面程序先定义了一个带泛型声明的接口,接下来三行粗体字代码分别示范了在匿名内部类中使用菱形语法。第一行粗体字代码声明变量时明确地将泛型指定为String类型,因此在该匿名内部类中T类型就代表了String类型;第二行粗体字代码声明变量时使用通配符来代表泛型(相当于通配符的上限为Object),因此系统只能推断出T代表Object,所以在该匿名内部类中T类型就代表了Object类型;第三行粗体字代码声明变量时使用了带上限(上限是Number)的通配符,因此系统可以推断出T代表Number类。

无论哪种方式,Java9都允许在使用匿名内部类时使用菱形语法

第9章 泛型 前言

本章要点

  • 编译时类型检查的重要性
  • 使用泛型实现编译时进行类型检查
  • 定义泛型接口、泛型类
  • 派生泛型接口、泛型类的子类、实现类
  • 使用类型通配符
  • 设定类型通配符的上限
  • 设定类型通配符的下限
  • 设定泛型形参的上限
  • 在方法签名中定义泛型
  • 泛型方法和类型通配符的区别与联系
  • 泛型方法与方法重载
  • Java 8改进的类型推断
  • 擦除与转换
  • 泛型与数组

本章概述

Java 5增加泛型

本章的知识可以与前一章的内容补充阅读,因为Java 5增加泛型支持在很大程度上都是为了让集合能记住其元素的数据类型

没有泛型之前集合中的对象当成Object处理

在没有泛型之前,一旦把一个对象”丢进”Java集合中,集合就会忘记对象的类型,把所有的对象当成Object类型处理。当程序从集合中取出对象后,就需要进行强制类型转换,这种强制类型转换不仅使代码臃肿,而且容易引起ClassCastExeception异常。

有了泛型集合可以检查元素类型

增加了泛型支持后的集合,完全可以记住集合中元素的类型,并可以在编译时检查集合中元素的类型,如果试图向集合中添加不满足类型要求的对象,编译器就会提示错误

增加泛型后的集合,可以让代码更加简洁,程序更加健壮(Java泛型可以保证如果程序在编译时没有发岀警告,运行时就不会产生ClassCastException异常)。

枚举类和反射的泛型

除此之外,Java泛型还增强了枚举类反射等方面的功能,泛型在反射中的用法,将在第18章中介绍。

本章不仅会介绍如何通过泛型来实现编译时检查集合元素的类型,而且会深入介绍Java泛型的详细用法,包括定义泛型类泛型接口,以及类型通配符泛型方法等知识

8.10 本章小结

本章详细介绍了Java集合框架的相关知识。

  • 本章从Java的集合框架体系开始讲起,概述了Java集合框架的4个主要体系:setListQueueMap,并简述了集合在编程中的重要性。
  • 本章详细介绍了Java 8对集合框架的改进,包括使用Lambda表达式简化集合编程,以及集合的Stream编程等。
  • 本章细致地讲述了SetListQueueMap接口及各实现类的详细用法,并深入分析了各种实现类实现机制的差异,并给出了选择集合实现类时的原则。
  • 本章从原理上剖析了Map结构特征,以及Map结构和SetList之间的区别及联系。
  • 本章最后通过梭哈游戏示范了Collections工具类的基本用法。

本章练习

  1. 创建一个Set集合,并用Set集合保存用户通过控制台输入的20个字符串。
  2. 创建一个List集合,并随意添加10个元素。然后获取索引为5处的元素;再获取其中某2个元素的索引;再删除索引为3处的元素。
  3. 3.给定["a","b","a","b","c","a","b","c","b"]字符串数组,然后使用Mapkey来保存数组中字符串元素,value保存该字符串元素的出现次数,最后统计出各字符串元素的出现次数
  4. 将本章未完成的梭哈游戏补充完整,不断地添加梭哈规则,开发一个控制台的梭哈游戏。

8.9 烦琐的接口Enumeration

Enumeration接口是Iterator迭代器的”古老版本”,从JDK1.0开始,Enumeration接口就已经存在了(IteratorJDK1.2才出现)。Enumeration接口只有两个名字很长的方法。

Enumeration接口方法

方法 描述
boolean hasMoreElements() 如果此迭代器还有剩下的元素,则返回true
Object nextElement() 返回该迭代器的下一个元素,如果还有的话(否则抛出异常)。

通过这两个方法不难发现,Enumeration接口中的方法名称冗长,难以记忆,而且没有提供Iteratorremove方法。如果现在编写Java程序,应该尽量采用Iterator迭代器,而不是用Enumeration迭代器

尽量采用Iterator 不推荐使用Enumeration

Java之所以保留Enumeration接口,主要是为了照顾以前那些”古老”的程序,那些程序里大量使用了Enumeration接口,如果新版本的Java里直接删除Enumeration接口,将会导致那些程序全部出错。在计算机行业有一条规则:加入任何规则都必须慎之又慎,因为以后无法删除规则。

实际上,前面介绍的Vector(包括其子类Stack)Hashtable两个集合类,以及另一个极少使用的Bitset,都是从JDK1.0遗留下来的集合类,而Enumeration接口可用于遍历这些”古老”的集合类。

而对于ArrayListHashMap等集合类,已经不支持Enumeration迭代器了。

Enumeration迭代Vector和Hashtable

下面程序示范了如何通过Enumeration接口来迭代VectorHashtable

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import java.util.*;

public class EnumerationTest
{
public static void main(String[] args)
{
Vector vector = new Vector();
vector.add("疯狂Java讲义");
vector.add("轻量级Java EE企业应用实战");
Hashtable scores = new Hashtable();
scores.put("语文" , 78);
scores.put("数学" , 88);

Enumeration em = vector.elements();
while (em.hasMoreElements())
{
System.out.println(em.nextElement());
}
Enumeration keyEm = scores.keys();
while (keyEm.hasMoreElements())
{
Object key = keyEm.nextElement();
System.out.println(key + "--->"
+ scores.get(key));
}
}
}

运行结果:

1
2
3
4
疯狂Java讲义
轻量级Java EE企业应用实战
语文--->78
数学--->88

上面程序使用Enumeration迭代器来遍历VectorHashtable集合里的元素,其工作方式与Iterator迭代器的工作方式基本相似。但使用Enumeration迭代器时方法名更加冗长。

而且,Enumeration迭代器只能遍历VectorHashtable这种古老的集合,因此通常不要使用它。除非在某些极端情况下,不得不使用Enumeration,否则都应该选择Iterator迭代器

8.8.4 设置不可变集合

Collections提供了如下三类方法来返回一个不可变的集合。

empty方法

这类方法返回一个空的、不可变的集合对象

方法 描述
static <T> Enumeration<T> emptyEnumeration() Returns an enumeration that has no elements.
static <T> Iterator<T> emptyIterator() Returns an iterator that has no elements.
static <T> List<T> emptyList() Returns an empty list (immutable).
static <T> ListIterator<T> emptyListIterator() Returns a list iterator that has no elements.
static <K,​V> Map<K,​V> emptyMap() Returns an empty map (immutable).
static <K,​V> NavigableMap<K,​V> emptyNavigableMap() Returns an empty navigable map (immutable).
static <E> NavigableSet<E> emptyNavigableSet() Returns an empty navigable set (immutable).
static <T> Set<T> emptySet() Returns an empty set (immutable).
static <K,​V> SortedMap<K,​V> emptySortedMap() Returns an empty sorted map (immutable).
static <E> SortedSet<E> emptySortedSet() Returns an empty sorted set (immutable).

singleton方法

这类方法返回一个只包含指定对象(只有一个或一项元素)的、不可变的集合对象

方法 描述
static <T> Set<T> singleton(T o) Returns an immutable set containing only the specified object.
static <T> List<T> singletonList(T o) Returns an immutable list containing only the specified object.
static <K,​V> Map<K,​V> singletonMap(K key, V value) Returns an immutable map, mapping only the specified key to the specified value.

unmodifiable方法

返回指定集合对象的不可变视图

方法 描述
static <T> Collection<T> unmodifiableCollection(Collection<? extends T> c) Returns an unmodifiable view of the specified collection.
static <T> List<T> unmodifiableList(List<? extends T> list) Returns an unmodifiable view of the specified list.
static <K,​V> Map<K,​V> unmodifiableMap(Map<? extends K,​? extends V> m) Returns an unmodifiable view of the specified map.
static <K,​V> NavigableMap<K,​V> unmodifiableNavigableMap(NavigableMap<K,​? extends V> m) Returns an unmodifiable view of the specified navigable map.
static <T> NavigableSet<T> unmodifiableNavigableSet(NavigableSet<T> s) Returns an unmodifiable view of the specified navigable set.
static <T> Set<T> unmodifiableSet(Set<? extends T> s) Returns an unmodifiable view of the specified set.
static <K,​V> SortedMap<K,​V> unmodifiableSortedMap(SortedMap<K,​? extends V> m) Returns an unmodifiable view of the specified sorted map.
static <T> SortedSet<T> unmodifiableSortedSet(SortedSet<T> s) Returns an unmodifiable view of the specified sorted set.

上面三类方法的参数是原有的集合对象,返回值是该集合的”只读”版本。通过Collections提供的三类方法,可以生成”只读”的CollectionMap

实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import java.util.*;

public class UnmodifiableTest
{
public static void main(String[] args)
{
// 创建一个空的、不可改变的List对象
List unmodifiableList = Collections.emptyList();
// 创建一个只有一个元素,且不可改变的Set对象
Set unmodifiableSet = Collections.singleton("疯狂Java讲义");
// 创建一个普通Map对象
Map scores = new HashMap();
scores.put("语文" , 80);
scores.put("Java" , 82);
// 返回普通Map对象对应的不可变版本
Map unmodifiableMap = Collections.unmodifiableMap(scores);
// 下面任意一行代码都将引发UnsupportedOperationException异常
//①
unmodifiableList.add("测试元素");
//②
unmodifiableSet.add("测试元素");
//③
unmodifiableMap.put("语文" , 90);
}
}

上面程序的三行粗体字代码分别定义了一个空的、不可变的List对象,一个只包含一个元素的、不可变的Set对象和一个不可变的Map对象。不可变的集合对象只能访问集合元素,不可修改集合元素。所以上面程序中①②③处的代码都将引发UnsupportedOperationException异常。

8.8.3 同步控制

Collections类中提供了多个synchronizedXxx()方法,该方法可以将指定集合包装成线程同步的集合,从而可以解决多线程并发访问集合时的线程安全问题。

Java中常用的集合框架中的实现类HashSetTreeSetArrayListArrayDequeLinkedListHashMapTreeMap都是线程不安全的。如果有多个线程访问它们,而且有超过一个的线程试图修改它们,则存在线程安全的问题。

将集合包装成线程安全的方法

Collections提供了多个类方法可以把它们包装成线程同步的集合,如下表所示。

方法 描述
Collections.synchronizedCollection(Collection collection) Collection实例包装成线程安全的Collection
Collections.synchronizedList(List list) List实例包装成线程安全的List
Collections.synchronizedSet(Set set) Set实例包装成线程安全的Set
Collections.synchronizedMap(Map map) Map实例包装成线程安全的Map
方法 描述
Collections.synchronizedNavigableMap(NavigableMap<K,​V> m) Returns a synchronized (thread-safe) navigable map backed by the specified navigable map.
Collections.synchronizedNavigableSet(NavigableSet<T> s) Returns a synchronized (thread-safe) navigable set backed by the specified navigable set.
Collections.synchronizedSortedMap(SortedMap<K,​V> m) Returns a synchronized (thread-safe) sorted map backed by the specified sorted map.
Collections.synchronizedSortedSet(SortedSet<T> s) Returns a synchronized (thread-safe) sorted set backed by the specified sorted set.

程序 将集合包装成线程安全的示例

下面的示例程序创建了4个线程安全的集合对象。

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

public class SynchronizedTest
{
public static void main(String[] args)
{
// 下面程序创建了四个线程安全的集合对象
Collection c = Collections
.synchronizedCollection(new ArrayList());
List list = Collections.synchronizedList(new ArrayList());
Set s = Collections.synchronizedSet(new HashSet());
Map m = Collections.synchronizedMap(new HashMap());
}
}

如何获取线程安全的集合

在上面示例程序中,直接将新创建的集合对象传给了CollectionssynchronizedXxx()方法作为参数,这样就可以直接获取ListSetMap的线程安全实现版本

8.8.2 查找 替换 操作

Collections还提供了如下常用的用于查找、替换集合元素的类方法。

二分查找

方法 描述
static <T> int binarySearch(List<? extends Comparable<? super T>> list, T key) 使用二分搜索法搜索指定的List集合,以获得指定对象在List集合中的索引。如果要使该方法可以正常工作,则必须保证List中的元素已经处于有序状态
static <T> int binarySearch(List<? extends T> list, T key, Comparator<? super T> c) Searches the specified list for the specified object using the binary search algorithm.

查找最大最小值

方法 描述
static <T extends Object & Comparable<? super T>> T max(Collection<? extends T> coll) 根据元素的自然顺序(基于自然排序),返回给定集合中的最大元素
static <T> T max(Collection<? extends T> coll, Comparator<? super T> comp) 根据第二个Comparator参数指定的顺序,返回给定集合中的最大元素。
static <T extends Object & Comparable<? super T>> T min(Collection<? extends T> coll) 根据元素的自然顺序,返回给定集合中的最小元素。
static <T> T min(Collection<? extends T> coll, Comparator<? super T> comp) 根据第二个Comparator参数指定的顺序,返回给定集合中的最小元素

填充或者替换List

方法 描述
static <T> void fill(List<? super T> list, T obj) 使用指定元素object替换指定List集合中的所有元素。
static <T> boolean replaceAll(List<T> list, T oldVal, T newVal) 使用一个新值newValue替换List对象的所有旧值oldValue.

统计一个元素出现的次数

方法 描述
static int frequency(Collection<?> c, Object o) 返回指定集合中指定元素的出现次数。

查找子List在源List中第一次或最后一次出现的索引

方法 描述
static int indexOfSubList(List<?> source, List<?> target) 返回target List对象在source List对象中第一次出现的位置索引;如果source List中没有出现这样的子List,则返回-1。
static int lastIndexOfSubList(List<?> source, List<?> target) 返回target List对象在source List对象中最后一次出现的位置索引;如果source List中没有出现这样的子List,则返回-1

实例

下面程序简单示范了Collections工具类的用法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import java.util.*;

public class SearchTest
{
public static void main(String[] args)
{
ArrayList nums = new ArrayList();
nums.add(2);
nums.add(-5);
nums.add(3);
nums.add(0);
System.out.println(nums); // 输出:[2, -5, 3, 0]
System.out.println(Collections.max(nums)); // 输出最大元素,将输出3
System.out.println(Collections.min(nums)); // 输出最小元素,将输出-5
Collections.replaceAll(nums , 0 , 1); // 将nums中的0使用1来代替
System.out.println(nums); // 输出:[2, -5, 3, 1]
// 判断-5在List集合中出现的次数,返回1
System.out.println(Collections.frequency(nums , -5));
Collections.sort(nums); // 对nums集合排序
System.out.println(nums); // 输出:[-5, 1, 2, 3]
//只有排序后的List集合才可用二分法查询,输出3
System.out.println(Collections.binarySearch(nums , 3));
}
}

运行结果如下:

1
2
3
4
5
6
3
-5
[2, -5, 3, 1]
1
[-5, 1, 2, 3]
3