1. Java集合概述
- Java集合类是一种特别有用的工具类, 可用于存储数量不等的对象, 并可以实现常用数据结构, 如队列,栈;集合类都在 java.util 包下, Java5后, 在
java.util.concurrent
包下提供支持线程的集合类 - Java集合还可用于保存具有映射关系的关联数组
- Java集合分四种: Set(无序, 不可重复的集合), List(有序的, 重复的集合), Map(代表具有映射关系的集合), Queue(代表一种队列集合实现)
- Java的集合类主要由两个接口派生而出:
Collection
和Map
, 他们是Java集合框架的根接口, 这两个接口包含一些子接口和实现类; Set和List和Queue接口是Collection接口派生的子接口 - 所有的Map实现类用于保存具有映射关系的数据(也就是关联数组), Map的实现类保存的都是 key-value 对, 也就是由 key 和 value 两个值组成, key 不可以重复, 而 value 可以重复
- 集合和数组的区别: 数组元素既可以是基本类型的值, 也可以是对象(实际上保存的是对象的引用变量); 而集合只能保存对象(实际上只是保存的对象的引用变量--记住)
四种集合最常用的实现类
HashSet, TreeSet, ArrayList, ArrayDeque, LinkedList, HashMap, treeMap
访问集合的元素
- List集合元素直接通过索引来访问; 因为List是有序的集合
- Map集合的元素通过每项元素的key来访问其value; 因为Map集合的key值是不可重复的
- Set集合中的元素只能根据元素本身来访问; 因为Set集合的元素无序可重复
2. Collection和Iterator接口
- 由于 Collection 接口是 Set, List, Queue接口的父接口, 所以 Collection 接口定义的方法可用于操作Set, List, Queue集合
- ★ 集合就像容器, 实现生活中容器的功能, 可添加对象, 删除对象, 清空对象, 判断容器是否为空等, 集合类就为这些功能提供了对应的方法
Collection
接口定义的方法
- boolean add(Object o)
说明: 该方法用于向集合里添加一个元素, 如果集合对象被添加操作改变了(也就是添加成功), 就返回true - boolean addAll(Collection c)
说明: 该方法把集合c里所有的元素添加到指定的集合里, 如果集合对象被添加操作该变了, 就返回true - void clear()
说明: 清空集合里的所有元素, 将集合的长度变为0 - boolean contains(Object o)
说明: 返回集合里是否包含指定元素 - boolean containsAll(Collection c)
说明: 返回集合里是否包含集合c里的所有元素 - boolean isEmpty()
说明: 返回集合是否为空, 当集合长度为0时, 返回true, 否则返回false - Iterator iterator()
说明: 返回一个 Iterator对象, 用于遍历集合里的元素 - boolean remove(Object o)
说明: 删除集合中指定元素o, 当集合中包含了一个或多个元素o时, 该方法只删除第一个符合条件的元素, 该方法将返回true - boolean removeAll(Collection c)
说明: 从集合中删除集合c里包含的所有元素(相当于调用该方法的结合减集合c), 如果删除了一个或一个以上的元素, 则该方法返回true - boolean retainAll(Collection c)
说明: 从集合中删除集合c里不包含的元素(相当于把调用该方法的集合变成该集合和集合c的交集), 如果操作改变了调用方法的集合, 则该方法返回true - int size()
说明: 该方法返回集合里的元素个数 - Object[] toArray()
说明: 该方法把集合转换成一个数数组, 所有的集合元素变成对应的数组元素
示例代码
//例子:
// CollectionTest.java
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
//
public class CollectionTest{
public static void main(String[] args) {
// 创建一个 Collection 类型的 ArrayList 有序集合(都是通过Collection和Map的实现类创建的)
Collection c = new ArrayList();
// 添加元素
c.add("孙悟空");
// 虽然集合里不能放基本类型, 但Java支持自动装箱
c.add(6);
System.out.println("c集合的元素个数为: " + c.size());
// 删除指定元素
c.remove(6);
System.out.println("c集合的元素个数为: " + c.size());
// 判断是否包含指定的字符串
System.out.println("c集合是否包含\"孙悟空\"字符串: " + c.contains("孙悟空"));
c.add("轻量级 Java EE 企业级应用实战");
System.out.println("c集合的元素: " + c);
// 创建无序可重复的Set集合
Collection books = new HashSet();
books.add("轻量级 Java EE 企业级应用实战");
books.add("疯狂 Java 讲义");
System.out.println("c集合是否包含books集合? " + c.containsAll(books));
// 用c集合减去books集合的元素
c.removeAll(books);
System.out.println("c集合的元素: " + c);
// 删除c集合的所有元素
c.clear();
System.out.println("c集合的元素: " + c);
// 控制books集合里只剩下c集合里也包含的元素
books.retainAll(c);
System.out.println("books集合的元素: " + books);
}
}
// 程序说明:
// 1. 程序未使用泛型来限制集合里元素的种类, 编译会报警告
// 2. 所有的 Collection 实现类都重写了 toString() 方法, 该方法可以一次性地输出集合中的所有元素
2.1 使用Lambda表达式遍历集合
- Java8 为 Iterable 接口新增了一个 forEach(Consumer action)默认方法, 该方法所需的参数的类型是一个函数式接口, 而 Iterable 接口是 Collection 接口的父接口, 因此 Collection 接口也可以直接调用该方法
- 当程序调用 Iterable 的 forEach(Consumer action) 遍历集合元素时, 程序会依次将集合元素传给 Consumer 的 accept(T t) 方法 (该接口中唯一的抽象方法). 正因为 Consumer 是函数式接口, 因此可以使用Lambda表达式来遍历集合元素
示例代码
// 例子:
// CollectionEach.java
import java.util.Collection;
import java.util.HashSet;
//
public class CollectionEach {
public static void main(String[] args) {
// 创建一个无序不可重复的hashSet集合
Collection books = new HashSet();
books.add("java Note");
books.add("Python Note");
books.add("JavaScript Note");
books.add("Css Note");
// 调用 forEach()方法遍历(使用Lambda表达式遍历集合)
books.forEach(obj -> System.out.println("迭代集合元素:" + obj));
}
}
2.2 Java8 Iterator遍历集合元素
- Iterator 主要用于遍历(即迭代访问) Collection 集合中的元素, Iterator 对象也被称之为迭代器; 而 Collection 系列集合, Map系列集合主要用于盛装其他对象
- Iterator 接口隐藏了各种 Collection 实现类的底层细节, 向应用程序提供了遍历 Collection 集合元素的统一编程接口
Iterator 接口里定义的方法:(例子1)
- boolean hasNext()
说明: 如果被迭代的集合元素还没有被遍历完, 则该方法返回true - Object next()
说明: 返回集合里的下一个元素 - void remove()
说明: 删除集合里的下一个元素 - void forEachRemaining(Consumer action)
说明: 这是Java8为 Iterator 新增的默认方法, 该方法可使用Lambda表达式来遍历集合元素
示例代码
// 例子1:
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
public class IteratorTest{
public static void main(String[] args){
// 创建一个无序不可重复的HashSet集合
Collection books = new HashSet();
// 向集合中添加元素
books.add("java Note");
books.add("Python Note");
books.add("JavaScript Note");
books.add("Css Note");
// 获取books集合对应的迭代器(获取集合的迭代器必须通过被迭代的集合对象)
Iterator it = books.iterator();
while (it.hasNext()){
// it.next()方法返回的数据类型是Object类型, 因此需要强制类型转换
String book = (String)it.next();
if(book.equals("Python Note")){
// 从集合中删除上一次 next() 方法返回的元素
it.remove();
// books.remove(book); // ②
}
book = "测试字符串"; // ①
}
// out: [java Note, JavaScript Note, Css Note]
System.out.println(books);
}
}
例子1代码说明:
- Iterator 仅用于遍历集合. Iterator 本身并不提供盛装对象的能力, 如果需要创建 Iterator 对象(程序中的 it), 则必须有一个被迭代的集合(程序中的books), 没有集合的 Iterator 没有存在的价值
- Iterator必须依附与Collection对象, 若有一个Iterator对象, 则必然有一个与之关联的 Collection对象, Iterator 提供了两个方法来迭代访问 Collection 集合里的元素, 并可通过 remove() 方法来删除集合中上一次next()方法放回的集合元素
- ①处的代码并没有改变books集合中的元素, 当使用Iterator 对集合元素进行迭代时, Iterator 并不是把集合元素本身(集合元素时引用变量)传给了迭代变量, 而是把集合元素的值(引用变量指向的堆内存的值)传给了迭代变量, 所以修改迭代变量的值对集合元素本身没有影响
- 当使用 Iterator 迭代访问 Collection 集合元素时, Collection 集合里的元素不能被改变, 只有通过 Iterator 的 remove() 方法删除上一次 next() 方法返回的集合元素才可以, 否则将引发异常
2.3 使用Lambda表达式遍历Iterator
- Java8 为 Iterator 新增的一个
forEachRemaining(Consumer action)
方法, Consumer参数是函数式接口, - 当程序调用 Iterator 的 forEachRemaining(Consumer action) 遍历集合元素时, 程序会依次将集合元素传给 Consumer 的 accept(T t)方法(该接口中唯一的抽象方法)
示例代码
// 例子:
public class IterctorEach{
public static void main(String[] args){
// 创建一个无序可不重复的HashSet集合
Collection books = new HashSet();
// 添加集合元素
books.add("java Note");
books.add("Python Note");
books.add("JavaScript Note");
books.add("Css Note");
// 获取books集合对应的迭代器
Iterator it = books.iterator()
// 使用Lambda表达式(目标类型是Consumer)来遍历集合元素
it.forEachRemaining(obj -> System.out.println("迭代集合元素: " + obj));
}
}
2.4 使用foreach循环遍历集合元素
示例代码
// 例子:
public class foreachTest{
public static void main(String[] args){
// 创建一个无序不可重复的HashSet集合
Collection books = new HashSet();
// 添加集合元素
books.add("java Note");
books.add("Python Note");
books.add("JavaScript Note");
books.add("Css Note");
for(Object obj : books){
// 此处的book变量也不是集合元素本身
// 将Object类型的变量强转为String类型
String book = (String)obj;
System.out.println(book);
if(book.equals("Python Note")){
// 下面的代码引发异常
books.remove(book); // ①
}
}
System.out.println(books);
}
}
// 程序说明:
// 1. foreach循环中的迭代变量也不是集合元素本身, 系统只是依次把集合元素的值赋给迭代变量. 因此在
// foreach循环中修改迭代变量的值也没有实际意义
// 2. 当使用 foreach 循环迭代访问集合元素时, 该集合也不能被改变, 否则将引发异常, 如①处
2.5 使用Java8新增的 Predicate 操作集合
removeIf(Predicate filter)
方法支持 批量删除 符合 filter 条件的所有元素; Predicate 也是函数式接口, 也可以使用Lambda表达式作为参数
示例代码
// 例子:
import java.util.Collection;
import java.util.HashSet;
import java.util.function.Predicate;
//
public class PredicateTest {
public static void main(String[] args) {
// 创建一个无序不可重复的HashSet集合
Collection books = new HashSet();
// 添加集合元素
books.add("java Note");
books.add("Python Note");
books.add("JavaScript Note");
books.add("Css Note");
//
// 使用Lambda表达式(目标类型是Predicate)过滤集合 (删除长度大于12的元素)
books.removeIf(ele -> ((String)ele).length() > 12);
System.out.println(books); // out: [java Note, Python Note, Css Note]
// 统计书名中包含"Python"子串的图书数量
System.out.println(calAll(books, ele -> ((String)ele).contains("Python"))); //out: 1
// 统计书名中包含"Css"子串的图书数量
System.out.println(calAll(books, ele -> ((String)ele).contains("Css"))); // out: 1
// 统计书名字符串长度大于10的图书数量
System.out.println(calAll(books, ele -> ((String)ele).length() > 10)); // out: 1
}
//
public static int calAll(Collection books, Predicate p) {
int total = 0;
for (Object obj : books){
// 使用 Predicate 的 test() 方法判断该对象是否满足 Predicate 指定条件
if (p.test(obj)){
total ++;
}
}
return total;
}
}
// 程序说明:
// 1. 上述程序先定义了一个calAll()方法, 该方法将会使用 Predicate 判断每个集合元素是否符合特定条件
// 该条件将通过 Predicate 参数动态传入, 上述四行输出行代码传入了四个Lambda表达式(目标类型都
// 是Predicate), 这样 calAll()方法就只会统计满足Predicate条件的图书
2.6 使用java8新增的 Stream 操作集合
- Java8 还新增了 Stream, IntStream, LongStream, DoubleStream 等流式API, 这些API代表多个支持串行和并行聚集操作的元素; Stream 是一个通用的流接口, 而IntStream, LongStream, DoubleStream则是代表元素类型为 int, long, double 的流
- Java8 还为上面的每个流API提供了对应的 Biulder, 如: Stream.Builder, IntStream.Builder, LongStream.Builder, DoubleStream.Builder, 可通过这些Builder 来创建对应的流
- Java8 允许使用流式API来操作集合, Collection接口提供了一个 stream()默认方法, 该方法可返回该集合对应的流, 接下来可通过流式API来操作集合元素, Stream可对集合元素进行整机的聚集操作
独立使用 Stream 的步骤:
- 使用 Stream 或 XxxStream 的 builder() 类方法创建该 Stream 对应的 Builder
- 重复调用 Builder 的 add() 方法向该流中添加元素
- 调用 Builder 的 build()方法获取对应的 Stream
- 调用 Stream 的聚集方法
中间方法, 末端方法, 有状态方法, 短路方法:
- 中间方法: 中间操作允许流保持打开状态, 并允许直接调用后续方法. 下面程序中的map()方法
- 末端方法: 末端方法是对流的最终操作; 当某个Stream执行末端方法后, 该流将会被"消耗"且不可再用; 下面程序中的sum(), count(), average() 方法都是末端方法
- 有状态方法: 这种方法会给流增加一些新的属性, 比如元素的唯一性, 元素的最大数量, 保证元素以排序的方式被处理等, 有状态方法往往需要更大的性能开销
- 短路方法: 短路方法可以尽早结束对流的操作, 不必检查所有的元素
Stream 常用的中间方法:
- filter(Predicate predicate)
说明: 过滤Stream 中不符合 predicate 的元素 - mapToXxx(ToXxxFunction mapper)
说明: 使用 ToXxxFunction 对流中的元素执行一对一的转换, 该方法返回的新流中包含了 ToXxxFunction转换生成的所有元素 - peek(Consumer action)
说明: 依次对每个元素执行一些操作, 该方法返回的流与原有流包含相同的元素, 主要用于调试 - distinct()
说明: 该方法用于排序流中所有重复的元素(判断元素重复的标准是使用equals()比较返回true), 这是一个有状态的方法 - sorted()
说明: 用于保证流中的元素在后续的访问中 处于有序状态, 是一个有状态的方法 - limit(long maxSize)
说明: 用于保证对该流的后续访问中最大允许访问的元素个数. 有状态的,短路方法
Stream 常用的末端方法:
- forEach(Consumer action)
说明: 遍历流中的元素, 对每个元素执行action - toArray()
说明: 将流中的所有元素转换为数组 - reduce()
说明: 该方法有三个重载版本: 都用于通过某种操作来合并流中的元素 - min()
说明: 返回流中所有元素的最小值 - max()
说明: 返回流中所有元素的最大值 - count()
说明: 返回流中所有元素的数量 - anyMatch(Prerdicate predicate)
说明: 判断流中是否至少包含一个元素符合Predicate条件 - allMatch(Predicate predicate)
说明: 判断流中是否每个元素都符合Predicate条件 - noneMatch(Predicate predicate)
说明: 判断流中没有一个元素符合Predicate条件 - findFirst()
说明: 返回流中的第一个元素 - findAny()
说明: 返回流中的任意一个元素
示例代码
// 例子1:
public class IntStreamTest{
public static void main(String[] args){
IntStream is = IntStream.builder()
.add(20)
.add(13)
.add(-2)
.add(18)
.build();
//
// 下面调用聚集方法的代码每次执行执行一行
System.out.println("is 所有元素的最大值: " + is.max().getAsInt());
System.out.println("is 所有元素的最小值: " + is.min().getAsInt());
System.out.println("is 所有元素的总和: " + is.sum());
System.out.println("is 所有元素的总数: " + is.count());
System.out.println("is 所有元素的平均值: " + is.average());
System.out.println("is 所有元素的平方是否都大于20: "
+ is.allMatch(ele -> ele * ele > 20) );
System.out.println("is 是否包含任何元素的平方大于20: "
+ is.anyMatch(ele -> ele * ele > 20) );
// 将 is 映射成一个新的 Stream, 新的 Stream 的每个元素是原 Stream 元素的 2倍加1
IntStream newIs = is.map(ele -> ele * 2 + 1);
// 使用方法引用的方式来遍历集合元素 ============== 19/09/16 的晚上我已经忘了方法引用~~~
newIs.forEach(System.out::println); // out: 41 27 -3 37
}
}
// 例子2:
import java.util.Collection;
import java.util.HashSet;
//
public class CollectionStream {
public static void main(String[] args) {
// 创建一个集合
Collection books = new HashSet();
books.add("java Note");
books.add("Python Note");
books.add("JavaScript Note");
books.add("Css Note");
//
//统计书名包含 Note 子串的图书
System.out.println(books.stream()
.filter(ele -> ((String)ele).contains("Note")).count()); // out: 4
//统计书名包含 Java 子串的图书
System.out.println(books.stream()
.filter(ele -> ((String)ele).contains("Java")).count()); // out: 1
// 统计书名字符串长度大于10的图书数量
System.out.println(books.stream()
.filter(ele -> ((String)ele).length() > 10).count()); // out: 2
// 先调用 Collection 对象的 stream()方法将集合转换为Stream
// 在调用 Stream 的 mapToInt() 方法获取原有Stream对应的 IntStream
books.stream().mapToInt(ele -> ((String)ele).length())
// 调用 forEach() 方法遍历 IntStream 中每个元素
.forEach(System.out::print); // out: 9 15 11 8
}
}
// 程序说明:
// 1. 程序只要调用 Collection 的 stream() 方法即可返回该集合对应的Stream, 接下来就可以通过Stream
// 提供的方法对所有的集合元素进行处理, 可以简化代码
// 2. 上面程序最后一条输出语句, 先调用Collection 对象的 stream() 方法(即: books.stream())将集合
// 转换为 Stream对象, 然后调用 Stream对象的mapToInt() 方法将其转换为 IntStream; 这个过程中
// mapToInt() 方法就是一个中间方法, 因此程序可以继续调用 IntStream 的 forEach 方法来遍历流
// 中的元素
3. Set集合
- Set集合不允许包含相同的元素, 如果试图把两个相同的元素加入同一个Set集合中, 则添加操作失败, add() 方法返回false, 且新元素不会被加入
- 着重学习Set接口的三个实现类: HashSet, TreeSet, EnumSet
3.1 HashSet 类
- HashSet是set接口的典型实现类,
HashSet按Hash算法来存储集合中的元素
- 当向 HashSet 集合存一个元素时, Hashset 会调用该对象的 hashCode() 方法来获取该对象的HashCode值,然后根据该hashCode值决定该对象在 HashSet 中的存储位置
- 如果两个元素通过 equals()方法比较返回 true, 但他们的 hashCode()方法返回值不相等, HashSet 将会把他们存储在不同的位置, 依然可以添加成功; 即
HashSet 集合判断两个元素相等的标准是两个对象通过equals()方法比较相等, 并且两个对象的hashCode()方法的返回值也相等
HashSet特点
- HashSet 不能保证元素的排列顺序, 顺序可能与添加顺序不同, 顺序也有可能发生变化
- HashSet 不是同步的, 如果多个线程同时访问一个Hashset, 假设有两个或两个以上线程同时修改了 Hashset 集合时, 则必须通过代码来保证其同步
- HashSet 集合的元素可以是 null
示例代码
// 例子1:
// 通过类A,B,C, 他们分别重写了equals(),hashCode()两个方法的一个或全部, 通过这个例子:理解HashSet
// 集合判断两个元素相同的标准(equals()和hashCode()都相等)
// 类 A 的equals()方法总是返回true, 没有重写hashCode()方法
class A{
public boolean equals(){
return ture;
}
}
// 类 B 的hashCode() 方法总是返回1, 但是没有重写euqals()方法
class B {
public int hashCode(){
return 1;
}
}
// 类 C 的 hashCode()方法总是返回2, 且equals()方法总是返回true
class C {
public boolean equals(){
return true;
}
public int hashCode(){
return 2;
}
}
public class HashSetTest{
public static void main(String[] args){
HashSet books = new HashSet();
// 分别向books添加两个A对象, 两个B对象, 两个C对象
books.add(new A());
books.add(new A());
books.add(new B());
books.add(new B());
books.add(new C());
books.add(new C());
System.out.println(books);
}
}
代码(例子1)说明
- 由于C类重写了equals()方法总是返回true, hashCode()方法总是返回2, 将导致HashSet把两个C对象当成同一个对象
- 即使两个A对象通过equals()方法返回true,但HashSet依然把他们当成两个对象; 即使两个B对象的hashCode返回相同值, 但HashSet依然把他们当成两个对象
- 注意: 如果两个对象通过equals()方法总是返回true,这两个对象的hashCode值也应该相等
- 类B的equals()返回true,但是hashCode不同;这将会导致HashSet会把两个对象保存在Hash表的不同位置从而是两个对象都可以添加成功, 这就与Set集合的规则起冲突了
- HashSet中每个能存储元素的"槽位"(slot)通常称为"桶"(bucket), 如果有多个元素的hashCode值相同但他们通过equals()方法比较返回false, 就需要在一个"桶"里放多个元素, 这样导致性能下降
- 如果向 HashSet 中添加一个可变对象后, 后面程序修改了该可变对象的实例变量, 则可能导致它与集合中的其他元素相同(即两个对象通过equals()方法比较返回true,两个对象的hashCode值也相等),这就有可能导致 HashSet 中包含两个相同的对象(下面例子2)
重写hashCode()方法的基本规则
- 在程序运行过程中, 同一个对象多次调用HashCode()方法应该返回相同的值
- 当两个对象通过equals()方法比较返回true时, 这两个对象的hashCode值也应该相等
- 对象中用作equals()方法比较标准的实例变量, 都应该用于计算hashCode值
hashCode 值的计算方式
实例变量类型 | 计算方式 |
---|---|
boolean | hashCode = (f?0:1); |
整数类型(四个) | hashCode = int(f); |
long | hashCode = (int)(f^(f>>>32)); |
float | hashCode = Float.flaotToIntBits(f); |
double | long i = Double.doubleToLongBits(f); hashCode = (int)(i^(i>>>32)); |
引用类型 | hashCode = f.hashCode(); |
重写hashCode()方法的步骤
- 把对象内每个有意义的实例变量(即每个参与equals()方法比较标准的实例变量)计算出一个int类型的
hashCode值 - 用第一步计算出的多个hashCode值组合计算出一个hashCode值返回, 为避免直接相加产生偶然相等,可以通过为各实例变量的hashCode值乘以任意一个质数后再相加,
如:return f1.hashCode() * 19 + (int)f2 * 32;
示例代码
// 例子2:
import java.util.HashSet;
import java.util.Iterator;
//
class R {
int count;
public R(int count){
this.count = count;
}
public String toString(){
return "R[count: " + count + " ]";
}
public boolean equals(Object obj){
if(this == obj){
return true;
}
if(obj != null && obj.getClass() == R.class){
R r = (R)obj;
return this.count == r.count;
}
return false;
}
public int hashCode(){
return this.count;
}
}
public class HashSetTest2{
public static void main(String[] args){
HashSet hs = new HashSet();
hs.add(new R(5));
hs.add(new R(-3));
hs.add(new R(9));
hs.add(new R(-2));
// 打印HashSet集合, 集合元素没有重复
System.out.println(hs);
// 取出第一个元素
Iterator it = hs.iterator();
R first = (R)it.next();
// 为第一个元素count实例变量赋值
first.count = -3; // ①
// 再次输出 HashSet 集合, 集合元素有重复元素
System.out.println(hs);
// 删除count为-3的R对象
hs.remove(new R(-3)); // ②
// 可以看到被删除了一个R元素
System.out.println(hs);
System.out.println("hs是否包含count为-3的R对象?"
+ hs.contains(new R(-3))); // 输出 false
System.out.println("hs是否包含count为-2的R对象?"
+ hs.contains(new R(-2))); // 输出 false
}
}
// 程序说明:
// 1. 当向HashSet中添加可变对象时, 如果修改HashSet集合中的对象, 有可能导致该对象与集合中的其他对象
// 相等, 从而导致HashSet无法准确访问该对象
3.2 LinkedHashSet 类
- LinkedHashSet 集合也是根据元素的hashCode值来决定元素的存储位置, 但它同时
使用链表维护元素的次序,这样使得元素看起来是以插入的顺序保存的
; 即:当遍历 LinkedHashSet 集合里的元素时, LinkedHashSet将会按元素的添加顺序来访问集合里的元素(但它依然是HashSet,不允许集合元素重复) - LinkedHashSet 需要维护元素的插入顺序, 因此性能略低于 HashSet 的性能, 但在迭代访问 Set 里的全部元素时将有很好的性能, 因此它以链表来维护内部顺序
- 一句话:
LinkedHashSet 集合的元素,访问元素的顺序和添加元素的顺序一致
示例代码
// 例子:
import java.util.LinkedHashSet;
//
public class LinkedHashSetTest {
public static void main(String[] args){
LinkedHashSet books = new LinkedHashSet();
books.add("python");
books.add("Java");
System.out.println(books); // out: [python, Java]
// 删除 python
books.remove("python");
// 重新添加 python
books.add("python");
System.out.println(books); // out: [Java, python]
}
}
3.3 TreeSet 类
- TreeSet是SortedSet接口的实现类, TreeSet可以确保集合元素处于排序状态
TreeSet采用红黑树的数据结构来存储集合元素
, TreeSet支持两种排序方式, 自然排序和定制排序,默认情况下,TreeSet采用自然排序- 因为TreeSet中的元素是有序的, 所以增加了访问第一个, 前一个, 后一个, 最后一个元素的方法, 和三个从TreeSet中截取子TreeSet的方法
- TreeSet 并不是根据插入的元素顺序来进行排序, 而是根据元素实际值的大小来进行排序
TreeSet 提供的方法
- Comparator comparator()
说明: 如果 TreeSet 采用了定制排序, 则该方法返回定制排序所使用的 Comparator; 如果 TreeSet 采用了自然排序, 则返回null - Object first()
说明: 返回集合中的第一个元素 - Object last()
说明: 返回集合中的最后一个元素 - Object lower(Object e)
说明: 返回集合中位于指定元素之前的元素(即小于指定元素的最大元素, 参考元素不需要是TreeSet集合里的元素) - Object higher(Object e)
说明: 返回集合中位于指定元素之后的元素(即大于指定元素的最小元素, 参考元素不需要是TreeSet集合里的元素) - SortedSet subSet(Object fromElement, Object toElement)
说明: 返回此Set的子集合,范围从fromElement(包含)到toElement(不包含) - SortedSet headSet(Object toElement)
说明: 返回此Set的子集, 由小于toElement的元素组成 - SortedSet tailSet(Object fromElement)
说明: 返回此Set集合的子集, 由大于或等于fromElement的元素组成
示例代码
// 例子:
// TreeSet 方法应用
import java.util.TreeSet;
//
public class TreeSetTest{
public static void main(String[] args){
TreeSet nums = new TreeSet();
// 向nums添加四个Integer对象
nums.add(5);
nums.add(2);
nums.add(10);
nums.add(-9);
// 输出集合元素看到集合元素已经排序
System.out.println(nums); // out: [-9, 2, 5, 10]
// 输出集合里的第一个元素
System.out.println(nums.first()); // out: -9
// 输出集合里的最后一位元素
System.out.println(nums.last()); // out: 10
// 返回小于4的子集, 不包含4
System.out.println(nums.headSet(4)); // out: [-9, 2]
// 返回大于等于5的子集
System.out.println(nums.tailSet(5)); // out: [5, 10]
// 返回大于等于-3 小于4的子集
System.out.println(nums.subSet(-3, 4)); // out: [2]
}
}
自然排序
- 原理:
treeSet调用集合元素的 compareTo(Object obj)方法来比较元素之间的大小关系, 然后将集合元素按升序排列, 这种方式就是自然排序
- 向TreeSet集合中添加元素时, 只有第一个元素无需实现Comparable接口, 后面添加的所有元素都必须实现Comparable接口; 但是这样的话, 当试图从TreeSet中取出元素时, 依然会引发ClassCastException异常(例子1)
- 所以注意: 大部分类在实现compareTo(Object obj)方法时, 都需要将被比较对象 obj 强制转换成相同的类型, 因为只有两个相同类的两个实例才会比较大小; 当试图把一个对象添加到TreeSet集合时, TreeSet会调用该对象的 compareTo(Object obj)方法与集合中的其他元素进行比较, 这就要求集合中的其他元素与该元素是同一个类的实例, 也就是说,
向TreeSet中添加的应该是同一个类的对象, 否则会引发ClassCastException异常
(例子2) - 总结: TreeSet正常运作的前提就是, TreeSet只能添加同一种类型的对象
TreeSet集合判断两个对象是否相等的唯一标准是: 两个对象通过compareTo(Object obj)方法比较是否返回0; 返回0则相等,否则不相等
- 如果向TreeSet中添加了一个可变对象, 并且后面程序修改了该可变对象的实例变量, 这将导致它与其他对象的大小顺序发生了改变, 但TreeSet不会再次调整他们的顺序, 甚至可能导致TreeSet中保存的这两个对象通过compareTo(Object obj)方法比较返回0, (例子4)
关于 Comparable接口
- 该接口里定义了compareTo(Object obj)方法, 该方法返回一个整数值, 实现该接口的类必须实现该方法, 实现了该接口的类的对象就可以比较大小
- 当一个对象调用该方法与另一个对象比价大小的时候, 例如 obj1.compareTo(obj2), 如果该方法返回0,则表明两个对象相等, 如果返回一个正整数, 则表明obj1大于obj2, 返回一个负整数, 则说明obj1小于obj2(例子3)
Comparable接口的一些常用实现类
- BigDecimal, BigInteger 以及所有的数值型对应的包装类:
说明: 按他们对应的数值大小进行比较 - Character
说明: 按字符的 UNICODE 值进行比较 - Boolean
说明: true对应的包装类实例大于false对应的包装类实例 - String
说明: 按字符串中字符的 UNICODE 值进行比较 - Date, Time
说明: 后面的时间, 日期比前面的时间日期大
示例代码
// 例子1:
// 试图将一个对象添加到TreeSet时, 没实现Comparable接口, 第二次添加时, 报错
class Err{}
public class TreeSetErrTest{
public static void main(String[] args){
TreeSet ts = new TreeSet();
// 向TreeSet集合中添加两个Err对象
ts.add(new Err());
ts.add(new Err()); // 在第二次添加时会报错
}
}
//
// 例子2:
public class TreeSetErrTest2{
public static void main(String[] args){
TreeSet ts = new TreeSet();
// 向TreeSet集合中添加两个Err对象
ts.add(new String("Python"));
ts.add(new Date()); // 这行代码引发异常, 因为两个对象是不同类型
}
}
//
// 例子3:
import java.util.TreeSet;
// Z 类实现Comparable接口
class Z implements Comparable{
int age;
//构造器
public Z (int age){
this.age = age;
}
//重写equals()方法, 总是返回True
public boolean equals(Object obj){
return true;
}
// 重写 compareTo(Object obj)方法, 总是返回1
public int compareTo(Object obj){
return 1;
}
}
public class TreeSetTest2{
public static void main(String[] args){
TreeSet set = new TreeSet();
Z z1 = new Z(6);
set.add(z1);
// 第二次添加同一个对象, 输出true, 表明添加成功
System.out.println(set.add(z1));
// 下面输出set集合, 将输出两个元素
System.out.println(set); // out: [chapter08.Z@2f333739, chapter08.Z@2f333739]
// 修改set集合的第一个元素的age变量
((Z)(set.first())).age = 9;
// 输出set集合的最后一个元素的age变量,将看到也变成了9
System.out.println(((Z)(set.last())).age); // out: 9
}
}
// 程序说明:
// 1. 注意: 如果两个对象通过equals()方法比较返回true时,这两个对象通过compareTo(Object obj)
// 方法比较应该返回0(返回0表示相等)
//
//例子4:
// R 类实现Comparable接口
class R implements Comparable{
int count;
//构造器
puublic R(int count){
this.count = count;
}
public String toString(){
return "R[count: " + count + " ]";
}
// 重写equals()方法,通过count来判断是否相等
public boolean equals(Object obj){
if (this == obj){
return true;
}
if (obj != null && obj.getClass() == R.class){
R r = (R)obj;
return r.count == this.count;
}
return false;
}
// 重写 compareTo(Object obj)方法, 根据count来比较大小
public int compareTo(Object obj){
R r = (R)obj;
return count > r.count ? 1: count < r.count ? -1 : 0;
}
}
public class TreeSetTest3{
public static void main(String[] args){
TreeSet ts = new TreeSet();
ts.add(new R(5));
ts.add(new R(-3));
ts.add(new R(9));
ts.add(new R(-2));
// 打印TreeSet集合, 集合元素是有序排列的
System.out.println(ts);
// 取出第一个元素
R first = (R)ts.first();
// 对第一个元素的count赋值
first.count = 20;
// 取出最后一个元素
R last = (R)ts.last();
// 对最后一个元素的count赋值, 与第二个元素的count相同
last.count = -2;
// 再次输出将看到 TreeSet 里的元素处于无序状态, 且由重复的元素
System.out.println(ts);
// 删除实例变量被改变的元素, 删除失败
System.out.println(ts.remove(new R(-2)));
System.out.println(ts);
// 删除实例变量没有被改变的元素, 删除成功
System.out.println(ts.remove(new R(5)));
System.out.println(ts);
}
}
// 程序说明:
// 1. 与 HashSet 类似的是, TreeSet中包含了可变对象, 当可变对象的实例变量被修改时, TreeSet
// 在处理这些对象时非常复杂, 而且容易错,
// 2. 为了程序更加健壮, 尽量不要修改放入HashSet和TreeSet集合中元素的关键实例变量
定制排序
- 通过Comparator接口实现定制排序, 该接口包含一个 int compare(T o1, T o2)方法, 该方法用于比较o1和o2的大小: 如果该方法返回正整数, 则表明o1大于o2, 如果该方法返回0,则表明o1等于o2; 如果该方法返回负整数, 则表明o1小于o2
- 要实现定制排序, 则需要在创建TreeSet集合对象时, 提供一个 Comparable 对象与该 TreeSet集合关联由该 Comparable 对象负责集合元素的排序逻辑; 由于 Comparable 是一个函数式接口, 可使用Lambda表达式来代替Comparator对象(例子1)
- 当通过 Comparator 对象(或lambda表达式)来实现TreeSet的定制排序时, 依然不可以向TreeSet中添加类型不同的对象, 否则也会引发ClassCastExxeption异常
- 使用定制排序时, TreeSet对集合元素排序不管集合元素本身的大小, 而是由 Comparator对象(或lambda表达式)负责集合元素的排序规则. TreeSet判断两个集合元素相等的标准是: 通过 Comparator (或者Lambda表达式)比较两个元素返回了0, 这样TreeSet不会把第二个元素添加到集合中
示例代码
// 例子1:
class M{
int age;
public M(int age){
this.age = age;
}
public String toString(){
return "M[age: " + age + " ]";
}
}
public class TreeSetTest4{
public static void main(String[] args){
// 此处 Lambda 表达式的目标类型是 Comparator
TreeSet ts = new TreeSet((o1, o2) -> {
M m1 = (M)o1;
M m2 = (M)o2;
// 根据M对象的 age 属性来决定大小, age 越大, M对象反而越小
return m1.age > m2.age ? -1 : m1.age < m2.age ? 1 : 0;
});
ts.add(new M(5));
ts.add(new M(-3));
ts.add(new M(9));
ts.add(new M(-10));
ts.add(new M(0));
// out: [M[age: 9 ], M[age: 5 ], M[age: 0 ], M[age: -3 ], M[age: -10 ]]
System.out.println(ts);
}
}
// 程序说明:
// 1. 上述例子中的Lambda表达式部分使用了目标类型为Comparator, 它负责ts集合的排序
// 2. 当把M对象添加到ts集合中时, 无须M类实现Comparable接口, 因此此时TreeSet无须通过M对象本身
// 来比较大小, 而是由与TreeSet关联的Lambda表达式来负责集合元素的排序
3.4 EnumSet 类
- EnumSet是一个专为枚举类设计的集合类, EnumSet中所有元素都必须是指定枚举类型的枚举值, 该枚举类型在创建EnumSet时显式或隐式的指定
- EnumSet 的集合元素也是有序的, EnumSet以枚举值在Enum类内的定义顺序来决定集合元素的顺序
- EnumSet 在内部以位向量的形式存储, 这种存储形式非常紧凑, 高效, EnumSet对象占用内存很小, 而且运行效率很高; 尤其是进行批量操作(如调用containsAll()和retainAll()方法)时, 如果参数只是EnumSet集合,则该批量操作的执行速度也非常快
- EnumSet集合不允许加入null元素, 如果试图插入null元素, 则会报NullPointerException异常; 判断EnumSet是否包含null元素和试图删除null元素都不会抛出异常(删除操作返回false)
- EnumSet类没有提供构造器来创建该类的实例, 程序应该通过它提供的类方法来创建EnumSet对象
EnumSet类创建EnumSet对象的方法
- EnumSet allOf(Class elementType)
说明: 创建一个包含指定枚举类里所有枚举值的EnumSet集合 - EnumSet complementOf(EnumSet s)
说明: 创建一个其元素类型与指定EnumSet里元素类型相同的EnumSet集合, 新EnumSet集合包含原EnumSet集合所不包含的, 此枚举类剩下的枚举值(即新EnumSet集合和原EnumSet集合的集合元素加起来就是该枚举类的所有枚举值) - EnumSet copyOf(Collection c)
说明: 使用一个普通集合来创建EnumSet集合(当复制Collection集合中的所有元素来创建新的EnumSet集合时要求Collection集合中的所有元素必须是同一个枚举类的枚举值)(例子2) - EnumSet copyOf(EnumSet s)
说明: 创建一个与指定 EnumSet 具有相同元素类型, 相同集合元素的EnumSet集合 - EnumSet noneOf(class elementType)
说明: 创建一个元素类型为指定枚举类型的空EnumSet - EnumSet of(E first, E... rest)
说明: 创建一个包含一个或多个枚举值的EnumSet集合, 传入的多个枚举值必须属于同一个枚举类 - EnumSet range(E from, E to)
说明: 创建一个包含从 from 到 to 枚举值范围内所有枚举值的EnumSet集合
示例代码
// 例子1:
// 如何使用EnumSet来保存枚举类的多个枚举值
import java.util.EnumSet;
//
enum Season {
SPRING, SUMMER, FALL, WINTER
}
public class EnumSetTest{
public static void main(String[] args){
// 创建一个EnumSet集合, 集合元素就是Season枚举类的全部枚举值
EnumSet es1 = EnumSet.allOf(Season.class);
System.out.println(es1); // 输出[SPRING, SUMMER, FALL, WINTER]
// 创建一个EnumSet空集合, 指定其集合元素是Season类的枚举类
EnumSet es2 = EnumSet.noneOf(Season.class);
System.out.println(es2); // 输出[]
// 手动添加两个元素
es2.add(Season.WINTER);
es2.add(Season.SPRING);
System.out.println(es2); // 输出 [SPRING, WINTER]
// 以指定枚举值创建EnumSet集合
EnumSet es3 = EnumSet.of(Season.SUMMER, Season.WINTER);
System.out.println(es3); // 输出[SUMMER, WINTER]
EnumSet es4 = EnumSet.range(Season.SUMMER, Season.WINTER);
System.out.println(es4); // 输出[SUMMER, FALL, WINTER]
// 新创建的EnumSet集合元素和es4集合元素有相同的类型
// es5集合元素 + es4集合元素 = Season雷剧类的全部枚举值
EnumSet es5 = EnumSet.complementOf(es4);
System.out.println(es5); // 输出[SPRING]
}
}
// 例子2:
public class EnumSetTest2{
public static void main(String[] args){
Collection c = new HashSet();
// 清空集合元素
c.clear();
c.add(Season.FALL);
c.add(Season.SPRING);
// 复制Collection集合中的所有元素来创建EnumSet集合
EnumSet es = EnumSet.copyOf(c);
System.out.println(es); // 输出[SPRING, FALL]
c.add("Python");
c.add("Love Java");
// 下面的代码出现异常: 因为c集合里的元素不是全部都为枚举值
es = EnumSet.copyOf(c);
}
}
3.5 各 Set 实现类的性能分析
- HashSet 的性能总是比TreeSet好(特别是最常用的添加, 查询元素等操作), 因为TreeSet需要额外的红黑树算法来维护集合元素的次序
- 只有当需要一个保持排序的Set时, 才应该使用TreeSet, 否则都应该使用 HashSet
- LinkedHashSet 对于普通的插入, 删除操作, LinkedHashSet 比 HashSet 要略微慢一点, 这是由于维护链表所带来的额外开销造成的, 但由于链表, 遍历LinkedHashSet会更快
- EnumSet 是所有的Set实现类中性能最好的, 但它只能保存同一个枚举类的枚举值作为集合元素
- 注意: Set 的三个实现类 HashSet, TreeSet, EnumSet都是线程不安全的, 多线程的情况下需要在创建时通过Collections工具类的 SynchronizedSortedSet方法来"包装"该Set集合, 以防止对Set集合的意外非同步访问如: SortedSet s = Collections.SynchronizedSortedSet(new TreeSet(...));
4. List 集合
- List集合代表的一个元素有序, 可重复的集合, 集合中每个元素都有其对应的顺序索引
- List 集合允许使用重复元素, 可以通过索引来访问指定位置的集合元素, List集合默认按照元素的添加顺序设置元素的索引
- List集合可以使用普通的for循环来遍历集合元素
- List 集合判断两个对象相等只要通过equals方法比较返回true即可(例子2)
4.1 Java8改进的List接口和ListIterator接口(例子1)
- void add(int index, Object element)
说明: 将元素element插入到List集合的index索引处 - boolean addAll(int index, Collection c)
说明: 将集合c包含的所有元素都插入到List集合的index索引处 - Object get(int index)
说明: 返回List集合index索引处的元素 - int indexOf(Object o)
说明: 返回对象 o 在List集合中第一次出现的位置索引 - int lastIndexOf(object o)
说明: 返回对象o在List集合中最后一次出现的位置索引 - Object remove(int index)
说明: 删除并返回index索引处的元素 - Object set(int index, Object element)
说明: 将index所引出的元素替换成element对象, 返回被替换的旧元素(使用该方法时, 指定的索引必须是List集合的有效索引; 该方法并不会改变List集合的长度) - List subList(int fromIndex, int toIndex)
说明: 返回从索引fromIndex(包含)到索引toIndex(不包含)处所有集合元素组成的子集合 - void replaceAll(UnaryOperator operator)
说明: 根据operator指定的计算规则重新设置List集合的所有元素(例子3) - void sort(Comparator c)
说明: 根据Comparator参数对List集合的元素排序(例子3)
示例代码
// 例子1:
import java.util.ArrayList;
import java.util.List;
//
// List集合根据索引来访问元素的方法
public class LsitTest{
public static void main(String[] args) {
List books = new ArrayList();
// 向books集合中添加三个元素
books.add(new String("You Love Python"));
books.add(new String("Yes I Love Java"));
books.add(new String("Do U Want A Python Note"));
System.out.println(books);
// 将新字符串对象插入在第二个位置
books.add(1, new String("Crazy Java Note"));
for (int i = 0; i < books.size(); i++){
System.out.println(books.get(i));
}
// 删除第三个元素
books.remove(2);
System.out.println(books);
// 判断指定元素在List集合中的位置
System.out.println(books.indexOf(new String("Crazy Java Note")));
// 将第二个元素替换成新的字符串对象
books.set(1, new String("Hello world"));
System.out.println(books);
// 将books集合的第二个元素(包括)
// 到第三个元素(不包括)截取成子集合
System.out.println(books.subList(1, 2));
}
}
// 例子2:
import java.util.ArrayList;
import java.util.List;
//
class A {
public boolean equals(Object obj){
return true;
}
}
public class ListTest2{
public static void main(String[] args){
List books = new ArrayList();
// 向books集合中添加三个元素
books.add(new String("You Love Python"));
books.add(new String("Yes I Love Java"));
books.add(new String("Do U Want A Python Note"));
System.out.println(books);
// 删除集合中的A对象, 将导致第一个元素被删除
books.remove(new A()); // ①
System.out.println(books);
// 删除集合中的A对象, 再次删除集合中的第一个元素
books.remove(new A()); // ②
System.out.println(books);
}
}
// 程序说明:
// 1. 执行①处的代码时, 程序试图删除一个A对象, List将会调用该A对象的equals()方法依次与该集合元素进
// 行比较, 如果该equals()方法以某个集合元素作为参数时返回true, List将会删除该元素, A类重写了
// equals()方法, 该方法总是返回true, 所以每次从List集合中删除A对象时, 总是删除List集合中的第
// 一个元素
// 例子3:
import java.util.ArrayList;
import java.util.List;
//
public class ListTest3 {
public static void main(String[] args){
List books = new ArrayList();
// 向books集合中添加四个元素
books.add(new String("Python"));
books.add(new String("Java"));
books.add(new String("JavaScript"));
books.add(new String("Linux"));
// 使用目标类型为comparator的Lambda表达式对List集合排序
books.sort((o1, o2) -> ((String)o1).length() - ((String)o2).length()); // ①
System.out.println(books); // out: [Java, Linux, Python, JavaScript]
// 使用目标类型为UnaryOperator的Lambda表达式来替换集合中所有元素
// 该lambda表达式控制使用每个字符传的长度作为新的集合元素
books.replaceAll(ele -> ((String)ele).length()); // ②
System.out.println(books); // out: [4, 5, 6, 10]
}
}
// 程序说明:
// 1. ①处的代码控制对List集合进行排序, 传给sort()方法的Lambda表达式指定的排序规则是: 字符串长
// 度越长, 字符串越大, 因此①处的代码执行完之后, List集合中的字符串会按由短到长的顺序排序
// 2. ②处的代码传给replaceAll()方法的Lambda表达式指定了替换集合元素的规则: 直接用集合元素(字
// 符串)的长度作为新的集合元素.
List集合的listIterator()方法
- 该方法返回一个ListIterator对象, ListIterator接口继承了Iterator接口, 提供了专门的操作List的方法
- 使用 ListIterator 迭代List集合时, 开始也需要采用正向迭代, 即先使用next()方法进行迭代, 在迭代过程中可以使用add()方法向上一次迭代元素的后面添加一个新元素
ListIterator接口在Iterotor接口基础上增加的方法:(例子1)
- boolean hasPrevious()
说明: 返回该迭代器关联的集合是否还有上一个元素 - Object previous()
说明: 返回该迭代器的上一个元素 - void add(Object o)
说明: 在指定位置插入一个元素
ListIterator 和 Iterator 的区别
- ListIterator增加了向前迭代的功能(Iterator只能向后迭代), 而且ListIterator还可通过add()方法向List集合中添加元素(Iterator只能删除元素)
示例代码
// 例子1:
import java.util.ArrayList;
import java.util.List;
import java.util.ListIterator;
//
public class ListIteratorTest {
public static void main(String[] args){
String[] books = new String[]{
"Python", "Java", "JavScript", "Linux"
};
List bookList = new ArrayList();
for (int i = 0; i < books.length; i++){
bookList.add(books[i]);
}
ListIterator lit = bookList.listIterator();
while(lit.hasNext()){
System.out.println(lit.next());
lit.add("----------分隔符-----------");
}
System.out.println("======下面开始反向迭代======");
while(lit.hasPrevious()){
System.out.println(lit.previous());
}
}
}
4.2 ArrayList 和 Vector 实现类
- ArrayList 和 Vector 是 List的实现类, 支持List接口的全部功能
- ArrayList 和 Vector 类都是基于数组实现的List类, 所以ArrayList 和 Vector类封装了一个动态的, 允许再分配的Object[] 数组
- ArrayList 或 Vector 对象使用 initialCapacity参数来设置该数组的长度, 当向ArrayList 或 Vector中添加大量元素时, 可使用 ensureCapacity(int minCapacity)方法一次性地增加 initialCapacity,这可以减少重分配的次数, 从而提高性能
- 如果知道 ArrayList 或 Vector 集合需要保存多少个元素, 则可以在创建他们时就指定 initialCapacity的大小; 如果创建空的 ArrayList 或 Vector 集合时不指定 initialCapacity参数, 则 Object[]数组的长度默认为10
ArrayList 和 Vector 重分配 Object[]数组的方法:
- void ensureCapacity(int minCapacity)
说明: 将 ArrayList 或 Vector 集合的 Object[] 数组长度增加大于或等于 minCapacity 值 - void trimToSize()
说明: 调整 ArrayList 或 Vector 集合的 Object[] 数组长度为当前元素的个数, 调用该方法可减少ArrayList 或 Vector 集合对象占用的存储空间
ArrayList 和 Vector 的区别:
- ArrayList 是线程不安全的, 当多个线程访问同一个 ArrayList 集合时, 如果有超过一个线程修改了 ArrayList 集合, 则程序必须手动保证该集合的同步性;
- Vector 集合则是线程安全的, 无须程序保证集合的同步性; 因为 Vector 是线程安全的, 所以性能略低与ArrayList集合
- 不推荐使用 Vector 的实现类
- Vector 提供一个 Stack子类, 用于模拟"栈"这种数据结构, "栈"通常是"后进先出"(LIFO)的容器; 不使用
- ArrayDeque 也是 List 的实现类, ArrayDeque 既实现了List接口, 也实现了Deque接口, 因此也可以作为栈来使用; 因为 ArrayDeque 底层是基于数组的实现, 所以性能也较好.
4.3 固定长度的 List
- 操作数组的工具类: Arrays, 提供一个 asList(Object... a)方法, 可以把一个数组或指定个数的对象转换成一个List集合, 这个List集合既不是ArrayList实现类的实例, 也不是Vector实现类的实例, 而是Arrays的内部类ArrayList的实例
- Arrays.ArrayList 是一个固定长度的List集合, 程序只能遍历访问该集合里的元素, 不能增加, 删除该集合里的元素
示例代码
import java.util.Arrays;
import java.util.List;
public class FixedSizeList {
public static void main(String[] args){
List fixedList = Arrays.asList("python", "java", "C");
// 获取fixedList的实现类, 将输出 java.util.Arrays$ArrayList
System.out.println(fixedList.getClass());
// 使用方法引用遍历集合元素
fixedList.forEach(System.out::println);
// 试图增加, 删除元素都会引发 UnsupportedOperationException 异常
// fixedList.add("New Python");
// fixedList.remove("python");
}
}
5. Queue 集合
- Queue 用于模拟队列这种数据结构, 队列通常指"先进先出"(FIFO)的容器, 队列的头部保存在队列中存放时间最长的元素, 队列的尾部保存在队列中存放时间最短的元素
- 新元素插入(offer)到队列的尾部,访问元素(poll)操作返回队列头部的元素; 通常队列不允许随机访问队列中的元素
- Queue 接口有一个 PriorityQueue 实现类; 也有一个 Deque 接口, Deque 接口代表一个"双端队列", 双端 队列可以同时从两端来添加, 删除元素, 因此 Deque 的实现类既可当成队列使用, 也可当成栈使用
- Deque 接口提供 ArrayDeque 和 LinkedList 两个实现类
Queue 接口中定义的方法:
- void add(Object e)
说明: 将指定元素加入此队列的尾部 - Object element()
说明: 获取队列头部的元素, 但是不删除该元素 - boolean offer(Object e)
说明: 将指定元素加入此队列的尾部, 当使用有容量限制的队列时, 此方法通常比 add(Object e)方法更好 - Object peek()
说明: 获取队列头部的元素, 但是不删除该元素, 如果此队列为空, 则返回null - Object poll()
说明: 获取队列头部的元素, 并删除该元素,如果此队列为空, 则返回null - Object remove()
说明: 获取队列头部的元素, 并删除该元素
5.1 PriorityQueue 实现类
- PriorityQueue 是一个比较标准的队列实现类, 因为PriorityQueue保存队列元素的顺序并不是按加入队列的顺序, 而是按照队列元素的大小进行重新排序
- 当调用 peek()方法或者poll()方法取出队列中的元素时, 并不是取出最先进入队列的元素, 而是取出队列中最小的元素
- PriorityQueue 不允许插入null 元素
PriorityQueue 的元素的两种排序方式:
- 自然排序: 采用自然顺序的 PriorityQueue 集合中的元素必须实现 Comparable 接口, 而且应该是同一个类的多个实例, 否则可能导致ClassCastException异常
- 定制排序: 创建 PriorityQueue 队列时, 传入一个 Comparator 对象, 该对象负责对队列中的所有元素进行排序; 采用定制排序时不要求队列元素实现 Comparable 接口
示例代码
import java.util.PriorityQueue;
public class PriorityQueueTest{
public static void main(String[] args){
PriorityQueue pq = new PriorityQueue();
// 下面的代码依次向pq中加入4个元素
pq.offer(6);
pq.offer(-3);
pq.offer(20);
pq.offer(18);
// 输出pq队列, 并不是按元素的加入顺序排序
System.out.println(pq);
// 访问队列中的第一个元素, 就是队列中最小的元素
System.out.println(pq.poll());
}
}
5.2 Deque 接口与 ArrayDeque 实现类
- Deque 接口是Queue 接口的子接口, 它代表一个双端队列, Deque 接口里定义了一些双端队列的方法, 这些方法允许从两端来操作队列的元素
- 通过 Deque 提供的 pop() 和 push() 方法, 还可以将双端队列当成一个栈来使用(例子1)
- ArrayDeque 是 Deque 的实现类, 是一个基于数组实现的双端队列, 创建 Deque 时同样可以指定一个numElements 参数, 该参数用于指定 Objcet[] 数组的长度; 如果不指定 numElements 参数的长度, Deque 底层数组的长度是 16
- ArrayList 和 ArrayDeque 两个集合类的实现机制基本相似, 他们底层都采用一个动态的, 可重分配的 Object[] 数组来存储集合元素, 当集合元素超出了该数组的容量时, 系统会在底层重新分配一个 Object[]数组来存储集合元素
- ArrayDeque 既可作为 "栈" 来使用, 也可以作为队列来使用, 当需要使用 "栈" 这种数据结构时, 优先使用ArrayDeque;
Deque 接口里定义的方法:
- void addFirst(Object obj)
说明: 将指定元素插入到该双端队列的开头 - void addLast(Object obj)
说明: 将指定元素插入到该双端队列的末尾 - Iterator descendingIterator()
说明: 返回该双端队列对应的迭代器, 该迭代器将以逆向顺序来迭代队列中的元素 - Object getFirst()
说明: 获取但不删除双端队列的第一个元素 - Object getLast()
说明: 获取但不删除双端队列的最后一个元素 - boolean offerFirst(Object e)
说明: 将指定元素插入到该双端队列的开头 - boolean offerLast(Object e)
说明: 将指定元素插入到该双端队列的末尾 - Object peekFirst()
说明: 获取但不删除该双端队列的第一个元素; 如果此双端队列为空, 则返回null - Object peekLast()
说明: 获取但不删除该双端队列的最后一个元素; 如果此双端队列为空, 则返回null - Object pollFirst()
说明: 获取并删除该双端队列的第一个元素; 如果此双端队列为空, 则返回null - Object pollLast()
说明: 获取并删除该双端队列的最后一个元素; 如果此双端队列为空, 则返回null - Object pop()
说明: (栈方法) pop 出该双端队列所表示的栈的栈顶元素, 相当于 removeFirst() - void push(Object e)
说明: (栈方法) 将一个元素 push 进该双端队列所表示的栈的栈顶, 相当于 addFirst(e) - Object removeFirst()
说明: 获取并删除该双端队列的第一个元素 - Objcet removeFirstOccurrence(Object o)
说明: 删除该双端队列的最后一次出现的元素 o - Objcet removeLast()
说明: 获取并删除该双端队列的最后一个元素 - boolean removeLastOccurrence(Object o)
说明: 删除该双端队列的最后一次出现的元素 o
示例代码
//例子1:
import java.util.ArrayDeque;
// 把 ArrayDeque 当成一个 "栈" 来使用
public class ArrayDequeStack{
public static void main(String[] args){
ArrayDeque stack = new ArrayDeque();
// 一次将三个元素 push 入 "栈"
stack.push("Python");
stack.push("Java");
stack.push("JavaScript");
System.out.println(stack); // out: [JavaScript, Java, Python]
// 访问第一个元素, 但并不将其pop出"栈", 输出: Python
System.out.println(stack.peek()); // out: JavaScript
System.out.println(stack); // out: [JavaScript, Java, Python]
System.out.println(stack.pop()); // out: JavaScript
System.out.println(stack); // out: [Java, Python]
}
}
// 例子2:
import java.util.ArrayDeque;
// 把 ArrayDeque 当成一个 "队列" 来使用 ("先进先出")
public class ArrayDequeQueue{
public static void main(String[] args) {
ArrayDeque queue = new ArrayDeque();
queue.push("Python");
queue.push("Java");
queue.push("JavaScript");
System.out.println(queue); // out: [JavaScript, Java, Python]
// 访问队列头部的元素, 但并不讲其 poll 出队列 "栈"
System.out.println(queue.peek()); // out: JavaScript
System.out.println(queue); // out: [JavaScript, Java, Python]
// poll 出第一个元素
System.out.println(queue.poll()); // out: JavaScript
System.out.println(queue); // out: [Java, Python]
}
}
5.3 LinkedList 实现类
- LinkedList 是 List 接口的实现类, 是一个List集合, 可以根据索引来随机访问集合中的元素
- LinkedList 也实现了Deque接口, 可以被当成双端队列来使用, 所以也可以被当作 "栈" 来使用, 也可以被当成队列来使用 (例子1)
- LinkedList 和 ArrayList, ArrayDeque 的实现机制完全不同, ArrayList, ArrayDeque 内部以数组的形式来保存集合中的元素, 因此随机访问集合元素时有较好的性能; 而 LinkedList 内部以链表的形式来保存集合中的元素, 因此随机访问集合元素时性能较差, 但是在插入, 删除元素时性能比较好(只需改变指针所指的地址即可). 虽然 Vector 也是以数组的形式来存储集合元素的, 但因为它实现了线程同步的功能(实现机制也不好), 所以各方面性能都比较差
- 对于所有内部基于数组的集合实现, 如 ArrayList, ArrayDeque 等, 使用随机访问的性能都比使用 Iterator迭代访问的性能要好, 因为随机访问会被映射成对数组元素的访问
示例代码
// 例子1:
import java.util.LinkedList;
public class LinkedListTest {
public static void main(String[] args) {
LinkedList books = new LinkedList();
// 将字符串元素加入队列的尾部
books.offer("Python");
// 将一个字符串元素加入栈的顶部
books.push("Java");
System.out.println(books);
// 将字符串元素添加到队列的头部(相当于栈的栈顶)
books.offerFirst("JavaScript");
// 以List的方式(按索引访问的方式)来遍历集合元素
for (int i = 0; i < books.size(); i++){
System.out.println("遍历中: " + books.get(i));
}
// 访问并不删除栈顶的元素
System.out.println(books.peekFirst());
// 访问并不删除队列的最后一个元素
System.out.println(books.peekLast());
// 将栈顶的元素弹出 "栈"
System.out.println(books.pop());
// 下面的输出将看到第一个元素被删除
System.out.println(books);
// 访问并删除队列的最后一个元素
System.out.println(books.pollLast());
System.out.println(books);
}
}
5.4 各种线性表的性能分析
- List 就是一个线性表接口, 而 LinkedList 和 ArrayList 是基于数组的线性表和基于链的线性表的两种典型实现; Queue 代表了队列, Deque 代表了双端队列(既可作为队列使用, 也可以作为栈使用)
- LinkedList 集合不仅提供了List的功能, 也提供了双端队列, 栈的功能
- ArrayList 的性能比 LinkedList 的性能要好
使用List集合注意点:
- 如果需要遍历List集合元素, 对于ArrayList, Vector 集合, 应该使用随机访问方法(get)来遍历集合元素, 这样性能更好; 对于LinkedList 集合, 则应该采用迭代器(Iterator)来遍历集合元素
- 如果需要经常执行插入, 删除操作来改变包含大量数据的 List 集合的大小, 可考虑使用 LinkedList 集合, 因为使用 ArrayList 和 Vector 集合可能需要经常重新分配内部数组的大小, 效果可能较差
- 如果有多个线程需要同时访问List集合中的元素, 可以考虑使用Collections将集合包装成线程安全的集合
6. Java 8 增强的 Map 集合
- Map 用于保存具有映射关系的数据, 因此Map集合里保存着两组值, 一组值用于保存Map里的Key, 另外一组值用于保存Map里的value
- Key 和 value 都可以是引用数据类型, key不允许重复(即同一个Map对象的任何两个key通过equals方法比较总是返回false)
- Map中的key和value之间存在单向的一对一关系, 即通过指定的key,总能找到唯一的, 确定的value; 从Map中取出数据时, 只要给出指定的key,你可以取出对应的value
- 如果把Map里的所有key放在一起来看, 他们就组成了一个Set集合(所有的key没有顺序, key和key之间不能重复), 所以Map包含了一个 keySet()方法, 用于返回Map里所有key组成的Set集合
- Map接口中有 HashMap, LinkedHashMap, SortedMap(接口), TreeMap, EnumMap等子接口和实现类
- 如果将Map中的所有value放在一起来看, 他们非常类似与一个List(元素与元素之间可以重复, 每个元素可以根据索引来查找), 只是Map中索引不再使用整数值, 而是以另外一个对象作为索引
- Map最典型的用法: 就是成对的添加, 删除key-value对, 接下来即可判断该Map中是否包含指定的key, 是否包含指定的value, 也可以通过Map提供的keySet()方法获取所有key组成的集合, 进而遍历Map中所有的key-value 对 (例子1)
Map接口中定义的方法:
- void clear()
说明: 删除该Map对象中所有的key-value对 - boolean containsKey(Object Key)
说明: 查询Map中是否包含指定的key, 如果包含则返回true - boolean containsValue(Object value)
说明: 查询Map中是否包含一个或多个value, 如果包含则返回true - Set entrySet()
说明: 返回Map中包含的 key-value 对所组成的Set集合, 每个集合元素都是 Map.Entry (Entry是Map的内部类)对象 - Object get(Object key)
说明: 返回指定key所对应的value; 如果此Map中不包含该key, 则返回null - boolean isEmpty()
说明: 查询该Map是否为空(即不包含任何Key-value对), 如果为空则返回true - Set KetSet()
说明: 返回该Map中所有key组成的Set集合 - Object put(Object key, Object value)
说明: t添加一个key-value 对, 如果当前Map中已有一个与该key相等的key-value对, 则新的 key-value对会覆盖原来的 key-value 对 - void putAll(Map m)
说明: 将指定的Map中的key-value对复制到本Map中 - Object remove(Object key)
说明: 删除指定key所对应的key-value对, 返回被删除key所关联的value, 如果key不存在, 则返回null - boolean remove(Object key, Objcet value)
说明: Java8增强的方法, 删除指定key,value所对应的key-value对, 如果从该Map中成功删除该key-value对, 该方法返回true, 否则返回false - int size()
说明: 返回该Map里的key-value对的个数 - Collection values()
说明: 返回该Map里所有value组成的 Collection
Entry类包含的方法(Entry是一个内部类, 该类封装了一个key-value对):
- Objcet getKey()
说明: 返回该Entry 里包含的key值 - Object getValue()
说明: 返回该Entry里包含的value值 - Object SetValue(V value)
说明: 设置该 Entry 里包含的value值, 并返回新设置的value值
示例代码
// 例子1:
import java.util.HashMap;
import java.util.Map;
public class MapTest{
public static void main(String[] args) {
Map map = new HashMap();
// 成对的添加多个 key-value对
map.put("Python", 50);
map.put("Java", 30);
map.put("JavaScript", 10);
// 多次放入的 key-value 对中value可以重复
map.put("Linux", 30);
// 如果放入重复的key, 则新的value会覆盖原有的value
// 如果新的value覆盖了原有的value, 该方法返回被覆盖的原有的value
System.out.println(map.put("Python", 55));
System.out.println(map); // out:{Java=30, Linux=30, JavaScript=10, Python=55}
// 判断是否包含指定的key
System.out.println("是否包含 Python 的key: " + map.containsKey("Python"));
// 判断是否包含指定的value
System.out.println("是否包含 55 的value: " + map.containsValue(55));
// 获取Map集合的所有key组成的集合, 通过遍历key来实现遍历所有的key-value对
for (Object key : map.keySet()){
// map.get(key)方法获取指定key对应的value
System.out.println(key + " ---> " + map.get(key));
}
// 根据key来删除key-value对
map.remove("Java");
System.out.println(map);
}
}
//程序说明:
//1. 添加key-value时Map中已有重复的key, 那么新添加的value会覆盖该key原来对应的value,并将返回原来
// 被覆盖的value
//2. HashMap 重写了toString()方法, 实际上所有的Map实现类都重写了toString()方法, 调用Map对象的
// toString()方法总是返回固定格式的字符串: {key1=value1, hey2=value2...}
6.1 Java 8 为 Map 新增的方法
- Objcet compute(Object key, BiFunction remappingFunction)
说明: 该方法使用 remappingFunction 根据原 key-value 对计算一个新 value; 只要新 value 不为 null, 就使用新 value 覆盖原 value; 如果原value不为 null, 但新 value 为 null, 则删除原key-value对;如果原value,新value同时为null, 那么该方法不改变任何key-value对, 直接返回null - Object computeIfAbsent(Object key, Function mappingFunction)
说明: 如果传给该方法的key参数在Map中对应的value为null, 则使用 mappingFunction 根据key计算一个新的计算结果, 如果计算结果不为null, 则用计算结果覆盖原有的value; 如果原Map远离啊不包含该key, 那么该方法可能会添加一组key-value对 - Object computeIfPresent(Object key, BiFunction remappingFunction)
说明: 如果传给该方法的key参数在Map中对应的value不为null, 该方法使用 remappingFunction 根据原 key,value计算一个新的结果, 如果计算结果不为null, 则使用该结果覆盖原来的value, 如果计算结果为null,删除原 key-value 对 - void forEach(BiConsumer action)
说明: java8新增的, 可以更简洁的遍历Map的key-value对 - Object getOrDefault(Object key, V defaultValue)
说明: 获取指定key对应的value,如果该key不存在, 则返回 defaultValue - Object merge(Object key, Object value, BiFunction remappingFunction)
说明: 该方法会先根据key参数获取该Map中对应的value; 如果获取的value为null, 则直接用传入的value覆盖原有的value(在这种情况下, 可能要添加一组key-value对); 如果获取的 value 为 null, 则使用 remappingFunction 函数根据原value, 新 value计算一个新的结果, 并用得到的结果去覆盖原有的value - Object putIfAbsent(Object key, Object value)
说明: 该方法自动检测指定key对应的value是否为null, 如果key对应的value为null, 该方法将会用新的value代替原来的null值 - Object replace(Object key, Object value)
说明: 将Map中指定key对应的value替换成新value, 与传统的put()方法不同的是, 该方法不可能添加新的key-value 对, 如果尝试途欢的key在原Map中不存在, 该方法不是添加key-value对, 而是返回null - replaceAll(BiFunction function)
说明: 该方法使用 BiFunction 对原 key-value 对执行计算, 并将计算结果作为该 key-value 对的value值
示例代码
// 例子:
import java.util.HashMap;
import java.util.Map;
public class MapTest2 {
public static void main(String[] args) {
Map map = new HashMap();
// 成对的添加多个key-value对
map.put("Python", 50);
map.put("Java", 30);
map.put("JavaScript", 10);
// 尝试替换key为 "newPython"的value, 由于原Map中没有对应的key
// 因此Map没有改变, 不会添加新的key-value对
map.replace("newPython", 55);
System.out.println(map); // out: {Java=30, JavaScript=10, Python=50}
// 使用原value与传入参数计算出来的结果覆盖原有的value
map.merge("Python", 50, (oldVal, param) -> (Integer)oldVal + (Integer)param);
// Python 的值比原来增加了10
System.out.println(map); // out: {Java=30, JavaScript=10, Python=100}
// 当key为 "Java" 对应的value为null(或不存在)时, 使用计算的结果作为新value
map.computeIfAbsent("newJava", (key) -> ((String)key).length());
// map中添加了newJava=4这组key-value对
System.out.println(map); // out: {Java=30, newJava=7, JavaScript=10, Python=100}
// 当key为 "newJava"对应的vlaue存在时,使用计算的结果作为新value
map.computeIfPresent("newJava", (key, value) -> (Integer)value * (Integer)value);
// map 中newJava=4 变成 newJava=16
System.out.println(map); // out: {Java=30, newJava=49, JavaScript=10, Python=100}
}
}
6.2 Java8 改进的 HashMap 和 Hashtable 实现类
- HashMap 和 Hashtable 都是 Map 接口的典型实现类, 他们之间的关系类似于 ArrayList 和 Vector的关系
- Hashtable是一个古老的Map实现类, 它包含两个 elements()(类似于Map接口中定义的values()方法)和keys()(类似于Map接口中定义的keySet()方法), 现在很少使用这两个方法
- 为了成功地在 HashMap, Hashtable中存储, 获取对象, 用作key的对象必须实现hashCode()方法和equals()方法;
- HashMap, Hashtable, HashSet判断两个key相等的标准也是: 两个key通过equals()方法比较返回true, 两 个key的 hashCode值也相等(例子2)
- 当使用自定义类作为 HashMap, Hashtable的key时, 如果重写该类的equals(Object obj)和hashCode()方法, 则应该保证两个方法的判断标准一致:当两个 key 通过 equals() 方法比较返回 true 时, 两个key的hashCode()返回值也应该相同
- 与HashSet类似的是, 如果使用可变对象作为HashMap, Hashtable的key, 并且程序修改了作为key的可变对象则也可能出现与HashSet类似的情形: 程序再也无法准确访问到Map中被修改过的key(例子3)
HashMap 和 Hashtable 的区别:
- Hashtable 是一个线程安全的 Map 实现, 但HashMap是线程不安全的实现, 所以HashMap比Hashtable的性能高一点; 但如果有多个线程访问同一个Map对象时, 使用Hashtable实现类会更好;
- Hashtable不允许使用null作为key和value, 如果试图把null值放进Hashtable中, 将会引发NullPointerException异常; 但是HashMap可以使用null作为key或value
- 由于HashMap里的key不能重复, 所以HashMap里最多只有一个key-value对的key为null, 但可以有无数多个key-value对的value为null (例子1)
- 虽然HashMap线程不安全, 但是可以通过Collections工具类把HashMap变成线程安全
- HashMap 和 Hashtable 中包含一个 containsValue() 的方法,用于判断是否包含指定的value()
- HashMap 和 Hashtable 判断两个value相等的标准是: 两个对象通过equals()方法比较返回true (例子2)
示例代码
// 例子1:
public class NullInHashMap{
public static void main(String[] args){
HashMap hm = new HashMap();
// 试图将两个key为null值的key-value对放入HashMap中
hm.put(null, null);
// hm.put(null, null);
// 将一个 value 为 null 值的 key-value 对放入 HashMap中
hm.put("a", null);
// 输出Map对象
System.out.println(hm);
}
}
// 例子2:
import java.util.Hashtable;
class A {
int count;
public A(int count){
this.count = count;
}
// 根据 count 的值来判断两个对象是否相等
public boolean equals(Object obj){
if (obj == this){
return true;
}
if (obj != null && obj.getClass() == A.class){
A a = (A)obj;
return this.count == a.count;
}
return false;
}
// 根据count计算hashCode值
public int hashCode(){
return this.count;
}
}
class B{
// 重写equals()方法, B对象与任何对象通过equals()方法比较都返回true
public boolean equals(Object obj){
return true;
}
}
public class HashtableTest{
public static void main(String[] args){
Hashtable ht = new Hashtable();
ht.put(new A(6000), "疯狂Java讲义");
ht.put(new A(87563), "轻量级Java EE 企业应用实战");
ht.put(new A(1232), new B());
System.out.println(ht);
// 只要两个对象通过equals()方法比较返回true
// Hashtable就认为他们是相等的value
// 由于hashtable中有一个B对象
// 它与任何对象通过equals()方法比较都想等, 所以下面输出true
System.out.println(ht.containsValue("测试字符串")); // out: ①
// 只要两个A对象的count相等, 他们通过equals()方法比较返回ture, 且hashCode值相等
// Hashtable即认为他们是相同的key, 所以下面输出true
System.out.println(ht.containsKey(87563)); // out:②
// 下面语句可以删除最后一对 key-value 对
ht.remove(new A(1232));
System.out.println(ht);
}
}
// 程序说明:
// 1. A 类判断两个A对象相等的标准是equals()方法比较返回true即可, 上面程序中的ht对象中包含了一个B
// 对象, 他与任何对象通过equals()方法比较总是返回true, 所以在①处代码处返回true, 在这种情况下
// 不管传给ht对象的 containsValue()方法参数是什么, 程序总是返回true
// 2. 根据Hashtable判断两个key相等的标准, 程序在②处也将输出true,因为两个A对象虽然不是同一个对象,
// 但他们通过equals()方法比较返回true, 且hashCode值相等, Hashtable即认为他们是同一个可以
// 例子3:
import java.util.HashMap;
import java.util.Iterator;
class A {
int count;
public A(int count) {
this.count = count;
}
// 根据 count 的值来判断两个对象是否相等
public boolean equals(Object obj) {
if (obj == this) {
return true;
}
if (obj != null && obj.getClass() == A.class) {
A a = (A) obj;
return this.count == a.count;
}
return false;
}
// 根据count计算hashCode值
public int hashCode() {
return this.count;
}
}
public class HashMapErrorTest {
public static void main(String[] args) {
HashMap ht = new HashMap();
ht.put(new A(6000), "疯狂Java讲义");
ht.put(new A(87563), "轻量级Java EE 企业应用实战");
// 获得Hashtable的key Set集合对应的 Iterator迭代器
Iterator it = ht.keySet().iterator();
// 取出Map中第一个key,并修改它的count值
A first = (A)it.next();
first.count = 87653; // out: ①
System.out.println(ht);
// 只能删除没有被修改过的key所对应的key-value对
ht.remove(new A(87563));
System.out.println(ht);
// 无法获取剩下的value, 下面的两行代码都将输出 null
System.out.println(ht.get(new A(87563)));
System.out.println(ht.get(new A(60000)));
}
}
6.3 LinkedHashMap 实现类
- LinkedHashMap 使用双向链表来维护key-value对次序(其实只需要考虑key的次序), 该链表负责维护Map的迭代顺序, 迭代顺序与key-value的插入顺序保持一致
- LinkedHashMap 可以避免对 HashMap, Hashtable里的 key-value对进行排序(只要插入key-value对时保持顺序即可), 同时又可避免使用TreeMap 所增加的成本
- LinkedHashMap 需要维护元素的插入顺序, 因此性能略低于HashMap的性能, 但因为它以链表来维护内部顺序, 所以在迭代访问Map里的元素时将由较好的性能(例子1)
示例代码
// 例子1:
public class LinkedHashMapTest{
public static void main(String[] args){
LinkedHashMap scores = new LinkedHashMap();
scores.put("语文", 80);
scores.put("英语", 82);
scores.put("数学", 76);
// 调用 forEach() 方法遍历scores里的所有key-value对
scores.forEach((key, value) -> System.out.println(key + " --> " + value));
}
}
6.4 使用 Properties 读写属性文件
- Properties 类是 Hashtable 类的子类, 在处理属性文件时特别方便(如win平台的ini文件就是属性文件),
- Properties 类可以把Map对象和属性文件关联起来, 从而可以把Map对象中的key-value对写入属性文件中,也可以把属性文件中的 "属性名=属性值"加载到Map对象中
- Properties 类相当于一个key, value都是String类型的Map
Properties 类提供的三个方法:
- String getProperty(String key)
说明: 获取 Properties 中指定属性名对应的属性值, 类似于Map的get(Object key)方法 - String getProperty(String key, String defaultValue)
说明: 该方法与前一个方法基本相似, 该方法多一个功能, 如果Properties中不存在指定的key时, 则该方法指定默认值 - Objcet setProperty(String key, String value)
说明: 设置属性值, 类似于Hashtable的put()方法 - void load(InputStream inStream)
说明: 从属性文件(以输入流表示)中加载key-value对, 把加载到的key-value对追加到Properties里(Properties 是Hashtable的子类, 它不保证key-value对之间的次序) - void store(OuputStream out, String comments)
说明: 将 Properties 中的 key-value 对输出到指定的属性文件(以输出流表示)中
示例代码
// 例子
import java.io.*;
import java.util.Properties;
public class PropertiesTest {
public static void main(String[] args) throws Exception{
Properties props = new Properties();
// 向 Properties 中添加属性
props.setProperty("username", "jefxff");
props.setProperty("password", "jefxff");
// 将Properties中的key-value对保存到a.ini文件中
props.store(new FileOutputStream("a.ini"), "comment line");
// 新建一个Properties对象
Properties props2 = new Properties();
// 向Properties中添加属性
props2.setProperty("gender", "male");
// 将a.ini文件汇总的key-value对追加到props2中
props2.load(new FileInputStream("a.ini"));
System.out.println(props2);
}
}
6.5 SortedMap 接口和 TreeMap 实现类
- treeMap 就是一个红黑树数据结构, 每个 key-value 即作为红黑树的一个节点;
- TreeMap 存储key-value对(节点)时, 需要根据key对节点进行排序; TreeMap可以保证所有的key-value对处于有序状态
- TreeMap判断两个key相等的标准: 两个key通过compareTo()方法返回0
- 如果使用自定义类作为TreeMap的key, 则应该保证: 两个key通过equals()方法比较返回true时, 他们通过CompareTo()方法比较应该返回0
TreeMap 两种排序方式:
- 自然排序: Treemap的所有key必须实现Comparable接口, 而且所有的key应该是同一类的对象, 否则将会抛出ClassCastException异常
- 定制排序: 创建 TreeMap 时, 传入一个 Comparator 对象, 该对象负责对 TreeMap中所有key进行排序, 采用定制排序时不要求Map的key实现Comparable接口
TreeMap类中的方法:
- Map.Entry firstEntry()
说明: 返回该Map中最小的key所对应的key-value对, 如果Map为空, 则返回null - Object firstKey()
说明: 返回该Map中的最小的key值, 如果该Map为空, 则返回null - Map.Entry lastEntry()
说明: 返回该Map中最大的key所对应的key-value对, 如果Map为空或不存在这样的key-value, 则返回null - Object lastKey()
说明: 返回该Map中的最大的key值, 如果该Map为空或不存在这样的key, 则返回null - Map.Entry higherEntry(Object key)
说明: 返回该Map中位于key后一位的key-value对(即大于指定key的最小key所对应的key-value对), 如果该Map为空, 则返回null - Object higherKey(Objcet ibj)
说明: 返回该Map中位于key后一位的key值(即大于指定key的最小key值), 如果该Map为空或不存在这样的 key-value对, 则都返回null - Object lowerEntry(Object key)
说明: 返回该Map中位于key前一位的key-value对(即小于指定key的最大key值所对应的key-value对), 如果该Map为空或不存在这样的key-value对, 则都返回null - Objcet lowerKey(Object obj)
说明: 返回该Map中位于key前一位的key值(即小于指定key的最大key值), 如果该Map为空或不存在这样的 key值, 则都返回null - NavigableMap subMap(Objcet fromKey, boolean fromInclusive, Object toKey, boolean toInclusive)
说明: 返回该Map的子Map, 其key的范围是从fromKey(是否包括取决于第二个参数)到toKey(是否包括取决于第四个参数) - SortedMap subMap(Object fromKey, Object toKey)
说明: 返回该Map的子Map, 其key的范围是从fromKey(包括)到toKey(不包括) - SortedMap tailMap(Object fromKey)
说明: 返回该Map的子Map, 其key的范围是大于fromKey(包括)的所有key - NavigableMap tailMap(Object fromKey, boolean inclusive)
说明: 返回该Map的子Map, 其key的范围是大于fromkey(是否包括取决于第二个参数)的所有key - SortedMap headMap(Object toKey)
说明: 返回该Map的子Map, 其key的范围是小于toKey(不包括)的所有key - NavigableMap headMap(Object fromKey, boolean inclusive)
说明: 返回该Map的子Map, 其key的范围是小于toKey(是否包括取决于第二个参数)的所有key
示例代码
// 例子
import java.util.TreeMap;
class R implements Comparable {
int count;
public R(int count){
this.count = count;
}
public String toString(){
return "R[count: " + count + " ]";
}
// 根据count来判断两个对象是否相等
public boolean equals(Object obj){
if (this == obj){
return true;
}
if (obj != null && obj.getClass() == R.class){
R r = (R)obj;
return r.count == this.count;
}
return false;
}
// 根据count属性值来判断两个对象的大小
public int compareTo(Object obj){
R r = (R)obj;
return count > r.count ? 1: count < r.count ? -1 : 0;
}
}
public class TreeMapTest{
public static void main(String[] args){
TreeMap tm = new TreeMap();
tm.put(new R(3), "Python");
tm.put(new R(-5), "Java");
tm.put(new R(9), "Linux");
System.out.println(tm);
// 返回该TreeMap的第一个Entry对象
System.out.println(tm.firstEntry());
// 返回该TreeMap的最后一个key值
System.out.println(tm.lastKey());
// 返回该TreeMap的比New R(2) 大的最小key值
System.out.println(tm.higherEntry(new R(2)));
// 返回该TreeMap的比New R(2)小的最大的key-value 对
System.out.println(tm.lowerEntry(new R(2)));
// 返回该 TreeMap 的子Map
System.out.println(tm.subMap(new R(-1), new R(4)));
}
}
6.6 WeakHashMap 实现类
- WeakHashMap 和 HashMap 的区别在于, HashMap 的 key保留了对实际对象的强引用, 这意味着只要该Hashmap对象不被销毁, 该HashMap的所有key所引用的对象就不会被垃圾回收, HashMap 也不会自动删除这些key所对应的key-value对; WeakHashMap 中的每个key对象只持有对实际对象的弱引用, 因此, 当垃圾回收了该key所对应的实际对象之后, WeakHashMap 会自动删除该key对应的 key-value 对
示例代码
// 例子1
public class WeakHashMapTest{
public static void main(String[] args){
WeakHashMap whm = new WeakHashMap();
// 向 WeakHashMap 中添加三个 key-value对
// 三个 key 都是匿名字符串对象(没有其他引用)
whm.put(new String("语文"), new String("良好"));
whm.put(new String("数学"), new String("及格"));
whm.put(new String("英语"), new String("中等"));
// 向 WeakHashMap 中添加一个 key-value 对
// 该 key 是一个系统缓存的字符串对象
whm.put("java", new String("中等"));
System.out.println(whm);
// 通知系统回收垃圾
System.gc();
System.runFinalization();
System.out.println(whm);
}
}
// 程序说明:
// 1. 添加前三个key-value对时, 这三个key都是匿名的字符串对象, WeakHashMap 只保留了他们的弱引用,
// 这样垃圾回收时会自动的删除这三个 key-value对
// 2. 第四句put代码因为key时一个字符串直接量(系统会自动保留对该字符串对象的强引用), 所以垃圾回收时
// 不会回收它
6.7 IdentityHashMap 实现类
- HashMap只要两个key通过equals()返回true,即可判定两个key相等, 而 IdentityHashMap 执行严格的相等(key1 == key2)时, 才认为两个key相等
- IdentityHashMap 提供了与 HashMap基本相似的方法, 也允许使用null作为key和value; 与 HashMap相似:IdentityHashMap 也不保证key-value对之间的顺序, 更不能保证他们的顺序随时间的退役保持不变
示例代码
// 例子
public class IdentityHashMapTest{
public static void main(String[] args){
IdentityHashMap ihm = new IdentityHashMap();
// 下面两对key-value通过 == 比较不相等, 所以不是相同的key, 两个都可以添加
ihm.put(new String("语文", 89));
ihm.put(new String("语文", 78));
// 下面的key是直接量, 所以通过 == 比较相等, 所以是相同的key, 只会添加一对key-value
ihm.put("java", 96));
ihm.put("java", 89));
System.out.println(ihm);
}
}
6.8 EnumMap 实现类
- EnumMap中的所有key都必须是单个枚举类的枚举值, 创建EnumMap时必须显式或隐式的指定它对应的枚举类
- EnumMap 在内部以数组的形式保存, 所以这种实现形式非常紧凑, 高效
- EnumMap 根据 key 的自然顺序(即枚举值在枚举类中的定义顺序)来维护key-value对之间的顺序
- EnumMap 不允许使用null作为key, 但允许使用null作为value
示例代码
// 例子
enum Season{
SPRING, SUMMER, FALL, WINTER
}
public class EnumMapTest{
public static void main(String[] args){
// 创建 EnumMap 对象, 该 EnumMap 的所有key都是 Season 枚举类的枚举值
EnumMap em = new EnumMap(Season.class);
em.put(Season.SUMMER, "夏日炎炎");
em.put(Season.SPRING, "春暖花开");
System.out.println(em);
}
}
6.9 各Map实现类的性能分析
- HashMap 和 Hashtable 的实现机制几乎一样, 但是Hashtable比较古老,集成了线程安全, 因此 HashMap 通常比 Hashtable 快
- 由于TreeMap采用红黑树来管理key-value对, 所以TreeMap要比HashMap 和 Hashtable慢(尤其是插入, 删除key-value对时)
- TreeMap一个好处就是其中的key-value对总是处于排序状态; 应用场景: 当TreeMap被填充之后, 调用keySet()取得key组成的Set, 再使用 toArray()方法生成key的数组, 接下来使用Arrays()的 binarySearch()方法在已排序的数组中快速的查询对象
- 一般的应用场景应该多使用HashMap, 因为其底层也是通过数组来存储key-value对的; 如果程序需要一个排好序的Map时, 则可以使用TreeMap
- LinkedHashMap 比 HashMap 要慢, 因为 LinkedHashMap 需要维护链表来保持Map中key-value的添加顺序
- EnumMap 的性能最好, 但是它只能使用同一个枚举类的枚举值作为key
7. 操作集合的工具类 Collections
7.1 List集合元素的排序操作
- void reverse(List list)
说明: 反转指定List集合中元素的顺序("倒序") - void shuffle(List list)
说明: 对List集合元素进行随机排序(shuffle 方法模拟了 "洗牌" 动作) - void sort(List list)
说明: 根据元素的自然顺序对指定List的集合元素按升序进行排序 - void sort(List list, Comparator c)
说明: 根据指定 Comparator 产生的顺序对List集合进行排序 - void swap(List list, int i, int j)
说明: 将指定List集合中i处元素和j处元素进行交换 - void rotate(List list, int distance)
说明: 当 distance 为正数时, 将list集合的后 distance 个元素"整体"移到前面; 当 distance 为负数时, 将list集合的前 distance 个元素"整体"移到后面, 该方法不会改变集合的长度
示例代码
// 例子
import java.util.ArrayList;
import java.util.Collections;
public class SortTest {
public static void main(String[] args){
ArrayList nums = new ArrayList();
nums.add(2);
nums.add(3);
nums.add(12);
nums.add(-2);
nums.add(99);
nums.add(0);
System.out.println(nums); // out: [2, 3, 12, -2, 99, 0]
// 反转
Collections.reverse(nums);
System.out.println(nums); // out: [0, 99, -2, 12, 3, 2]
// 自然排序
Collections.sort(nums);
System.out.println(nums); // out: [-2, 0, 2, 3, 12, 99]
// 洗牌
Collections.shuffle(nums);
System.out.println(nums); // out: [0, 12, 2, -2, 99, 3]
}
}
// 例子2:
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
// 梭哈游戏
public class ShowHand{
// 定义该游戏最多支持多少个玩家
private final int PLAY_NUM = 5;
// 定义扑克牌的所有花色和数值
private String[] types = {"方块", "草花", "红心", "黑桃"};
private String[] values = {"2", "3", "4", "5",
"6", "7", "8", "9", "10", "J", "Q", "K", "A"};
// cards 是一局游戏中剩下的扑克牌
private List<String> cards = new LinkedList<String>();
// 定义所有的玩家
private String[] players = new String[PLAY_NUM];
// 定义所有玩家手上的扑克牌
private List<String>[] playerCards = new List[PLAY_NUM];
/**
* 初始化扑克牌, 放入52张牌
* 并且使用shuffle方法模拟洗牌
*/
public void initCards(){
for (int i = 0; i < types.length; i++){
for (int j = 0; j < values.length; j++){
cards.add(types[i] + values[j]);
}
}
// 随机排列
Collections.shuffle(cards);
}
/**
* 初始化玩家为每个玩家分配用户名
*/
public void initPlayer(String... names){
if (names.length > PLAY_NUM || names.length < 2){
// 检验玩家数量
System.out.println("玩家数量不对!");
}
else{
// 初始化玩家用户名
for (int i = 0; i < names.length; i ++){
players[i] = names[i];
}
}
}
/**
* 初始化玩家手上的牌, 开始游戏时每个玩家手上的扑克牌为空
* 程序使用一个长度为0的LinkedList来表示
*/
public void initPlayerCards(){
for (int i = 0; i < players.length; i++){
if (players[i] != null && !players[i].equals("")){
playerCards[i] = new LinkedList<String>();
}
}
}
/**
* 输出全部扑克牌, 测试
*/
public void showAllCards(){
for (String card : cards){
System.out.println(card);
}
}
/**
* 派扑克牌
* @param first 最先派给谁
*/
public void deliverCard(String first){
// 调用ArrayUtils 工具类的search方法
// 查询处指定元素在数组中的索引
int firstPos = ArrayUtils.search(players, first);
for (int i = firstPos; i < PLAY_NUM; i++){
if (players[i] != null){
playerCards[i].add(cards.get(0));
cards.remove(0);
}
}
// 依次给位于该指定玩家之前的每位玩家派发扑克
for (int i = 0; i < firstPos; i++){
if (players[i] != null){
playerCards[i].add(cards.get(0));
cards.remove(0);
}
}
}
/**
* 输出玩家手上的扑克牌
* 实现该方法时, 应该控制每个玩家看不到别人的第一张牌, 没实现
*/
public void showPlayerCards(){
for (int i = 0; i < PLAY_NUM; i++){
//当玩家不为空时
if (players[i] != null){
// 输出玩家
System.out.println(players[i] + ": ");
// 遍历玩家手上的扑克牌
for (String card : playerCards[i]){
System.out.println(card + "\t");
}
}
System.out.println("\n");
}
}
public static void main(String[] args) {
ShowHand sh = new ShowHand();
sh.initPlayer("电脑玩家", "孙悟空");
sh.initCards();
sh.initPlayerCards();
// 测试所偶遇扑克牌
sh.showAllCards();
System.out.println("---------------------------");
// 下面从孙悟空开始派牌
sh.deliverCard("孙悟空");
sh.showPlayerCards();
/**
* 需要补充的内容:
* 1. 牌面最大的玩家下注
* 2. 其他玩家是否跟注
* 3. 游戏是否只剩下一个玩家? 如果是, 则他胜利
* 4. 如果已经是最后一张扑克牌, 则需要比较剩下玩家的牌面大小
*/
// 再次从 "电脑玩家" 开始派牌
sh.deliverCard("电脑玩家");
sh.showPlayerCards();
}
}
7.2 用于查找, 替换操作集合元素的类方法
- int binarySearch(List list, Object key)
说明: 使用二分搜索发搜索指定List集合, 以获得指定对象在List集合中的索引; 前提是List必须是有序的 - Object max(Collection coll)
说明: 根据元素的自然顺序, 返回给定集合中的最大元素 - Object max(Collection coll, Comparator comp)
说明: 根据 Comparator 指定的顺序, 返回给定集合中的最大元素 - Object min(Collection coll)
说明: 根据元素的自然顺序, 返回给定集合中的最小元素 - Object min(Collection coll, Comparator comp)
说明: 根据 Comparator 指定的顺序, 返回给定集合中的最小元素 - void fill(List list, Object obj)
说明: 使用指定元素obj替换指定List集合中的所有元素 - int frequency(Collection c, Object o)
说明: 返回指定集合中指定元素出现的次数 - int indexOfSubList(List source, List target)
说明: 返回子List对象在父List对象中第一次出现出现的位置索引, 如果父List没有出现这样的List,则返回-1 - int lastIndexOfSubList(List source, List target)
说明: 返回子List对象在父List对象中最以一次出现出现的位置索引, 如果父List没有出现这样的List,则返回-1 - boolean replaceAll(List list, Object oldVar, Objcet newVar)
说明: 使用一个新值newVar替换List对象的所有旧值oldVar
示例代码
// 例子
import java.util.ArrayList;
import java.util.Collections;
public class SearchTest {
public static void main(String[] args){
ArrayList nums = new ArrayList();
nums.add(2);
nums.add(3);
nums.add(12);
nums.add(-2);
nums.add(99);
nums.add(0);
System.out.println(nums); // out: [2, 3, 12, -2, 99, 0]
System.out.println(Collections.max(nums)); // out: 99
System.out.println(Collections.min(nums)); // out: -2
//使用55来代替nums中的0
Collections.replaceAll(nums, 0, 55);
System.out.println(nums); // out: [2, 3, 12, -2, 99, 55]
// 判断-2在List集合中出现的次数
System.out.println(Collections.frequency(nums, -2)); // out: [-2, 2, 3, 12, 55, 99]
Collections.sort(nums); // out:
System.out.println(nums);
System.out.println(Collections.binarySearch(nums, 99)); // out: 5
}
}
7.3 同步控制
- Collections工具类提供了多个synchronizedXxx()方法, 该方法可以将指定集合包装成线程同步的集合, 从而可以解决多线程并发访问集合时的线程不安全问题
- Java中常用的集合框架中的实现类HashSet, TreeSet, ArrayList, ArrauDeque, LinkedList, HashMap, 和 TreeMap都是线程不安全的; 如果有多个线程访问他们, 而且有超过一个的线程试图修改它们, 则存在线程安全的问题
示例代码
// 例子
// 创建四个线程安全(或线程同步)的集合对象
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 a = Collections.synchronizedMap(new HashMap());
}
}
7.4 设置不可变集合
Collections提供了三类方法来返回一个不可变的集合(只读版本)
- emptyXxx()
说明: 返回一个空的, 不可变的集合对象, 此处的集合既可以是List, 也可以是SortedSet或Set, 还可以是Map或者SortedMap - singletonXxx()
说明: 返回一个指包含指定对象(只有一个或一项元素)的, 不可变的集合对象, 集合可以是List或者Map - unmodifiableXxx()
说明: 返回指定集合对象的不可变视图, 此处的集合既可以是List, 也可以是SortedSet或Set, 还可以是Map或者SortedMap
示例代码
// 例子
public class UnmodifiableTest{
public static void main(String[] args){
// 创建一个空的, 不可变的List对象
List unmodifiableList = Collections.emptyList();
// 创建一个只有一个元素, 且不可改变的Set集合
Set unmodifiableSet = Collections.singleton("Python");
// 创建一个普通的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("英语", 59);
}
}