Java基础-集合

Java基础-集合

1.Java 数据结构

序号 类描述
1 Vector 该类和ArrayList非常相似,但是该类是同步的,可以用在多线程的情况,该类允许设置默认的增长长度,默认扩容方式为原来的2倍。
2 Stack 是Vector的一个子类,它实现了一个标准的后进先出的栈。
3 Dictionary 类是一个抽象类,用来存储键/值对,作用和Map类相似。
4 Hashtable 是 Dictionary(字典) 类的子类,位于 java.util 包中。
5 Properties 继承于 Hashtable,表示一个持久的属性集,属性列表中每个键及其对应值都是一个字符串。
6 BitSet 类创建一种特殊类型的数组来保存位值。BitSet中数组大小会随需要增加。

以上这些类是传统遗留的,在 Java2 中引入了一种新的框架-集合框架(Collection),我们后面再讨论。

枚举(Enumeration)

枚举(Enumeration)接口虽然它本身不属于数据结构,但它在其他数据结构的范畴里应用很广。枚举(The Enumeration)接口定义了一种从数据结构中取回连续元素的方式。

Enumeration 接口中定义了一些方法,通过这些方法可以枚举(一次获得一个)对象集合中的元素。

这种传统接口已被迭代器取代,虽然 Enumeration 还未被遗弃,但在现代代码中已经被很少使用了。尽管如此,它还是使用在诸如 Vector 和 Properties 这些传统类所定义的方法中,除此之外,还用在一些 API 类,并且在应用程序中也广泛被使用。 下表总结了一些 Enumeration 声明的方法:

序号 方法描述
1 boolean hasMoreElements( ) 测试此枚举是否包含更多的元素。
2 Object nextElement( ) 如果此枚举对象至少还有一个可提供的元素,则返回此枚举的下一个元素。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import java.util.Vector;
import java.util.Enumeration;

public class EnumerationTest {

public static void main(String args[]) {
Enumeration<String> days;
Vector<String> dayNames = new Vector<String>();
dayNames.add("Sunday");
dayNames.add("Monday");
dayNames.add("Tuesday");
dayNames.add("Wednesday");
dayNames.add("Thursday");
dayNames.add("Friday");
dayNames.add("Saturday");
days = dayNames.elements();
while (days.hasMoreElements()){
System.out.println(days.nextElement());
}
}
}

image-20230804145227483

位集合(BitSet)

位集合类实现了一组可以单独设置和清除的位或标志。该类在处理一组布尔值的时候非常有用,你只需要给每个值赋值一”位”,然后对位进行适当的设置或清除,就可以对布尔值进行操作了。

一个Bitset类创建一种特殊类型的数组来保存位值。BitSet中数组大小会随需要增加。这和位向量(vector of bits)比较类似。这是一个传统的类,但它在Java 2中被完全重新设计。

序号 方法描述
1 void and(BitSet set) 对此目标位 set 和参数位 set 执行逻辑与操作。
2 void andNot(BitSet set) 清除此 BitSet 中所有的位,其相应的位在指定的 BitSet 中已设置。
3 int cardinality( ) 返回此 BitSet 中设置为 true 的位数。
4 void clear( ) 将此 BitSet 中的所有位设置为 false。
5 void clear(int index) 将索引指定处的位设置为 false。
6 void clear(int startIndex, int endIndex) 将指定的 startIndex(包括)到指定的 toIndex(不包括)范围内的位设置为 false。
7 Object clone( ) 复制此 BitSet,生成一个与之相等的新 BitSet。
8 boolean equals(Object bitSet) 将此对象与指定的对象进行比较。
9 void flip(int index) 将指定索引处的位设置为其当前值的补码。
10 void flip(int startIndex, int endIndex) 将指定的 fromIndex(包括)到指定的 toIndex(不包括)范围内的每个位设置为其当前值的补码。
11 boolean get(int index) 返回指定索引处的位值。
12 BitSet get(int startIndex, int endIndex) 返回一个新的 BitSet,它由此 BitSet 中从 fromIndex(包括)到 toIndex(不包括)范围内的位组成。
13 int hashCode( ) 返回此位 set 的哈希码值。
14 boolean intersects(BitSet bitSet) 如果指定的 BitSet 中有设置为 true 的位,并且在此 BitSet 中也将其设置为 true,则返回 true。
15 boolean isEmpty( ) 如果此 BitSet 中没有包含任何设置为 true 的位,则返回 true。
16 int length( ) 返回此 BitSet 的”逻辑大小”:BitSet 中最高设置位的索引加 1。
17 int nextClearBit(int startIndex) 返回第一个设置为 false 的位的索引,这发生在指定的起始索引或之后的索引上。
18 int nextSetBit(int startIndex) 返回第一个设置为 true 的位的索引,这发生在指定的起始索引或之后的索引上。
19 void or(BitSet bitSet) 对此位 set 和位 set 参数执行逻辑或操作。
20 void set(int index) 将指定索引处的位设置为 true。
21 void set(int index, boolean v) 将指定索引处的位设置为指定的值。
22 void set(int startIndex, int endIndex) 将指定的 fromIndex(包括)到指定的 toIndex(不包括)范围内的位设置为 true。
23 void set(int startIndex, int endIndex, boolean v) 将指定的 fromIndex(包括)到指定的 toIndex(不包括)范围内的位设置为指定的值。
24 int size( ) 返回此 BitSet 表示位值时实际使用空间的位数。
25 String toString( ) 返回此位 set 的字符串表示形式。
26 void xor(BitSet bitSet) 对此位 set 和位 set 参数执行逻辑异或操作。
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
31
32
33
34
import java.util.BitSet;

public class BitSetTest {

public static void main(String args[]) {
BitSet bits1 = new BitSet(16);
BitSet bits2 = new BitSet(16);

// set some bits
for(int i=0; i<16; i++) {
if((i%2) == 0) bits1.set(i);
if((i%5) != 0) bits2.set(i);
}
System.out.println("Initial pattern in bits1: ");
System.out.println(bits1);
System.out.println("\nInitial pattern in bits2: ");
System.out.println(bits2);

// AND bits
bits2.and(bits1);
System.out.println("\nbits2 AND bits1: ");
System.out.println(bits2);

// OR bits
bits2.or(bits1);
System.out.println("\nbits2 OR bits1: ");
System.out.println(bits2);

// XOR bits
bits2.xor(bits1);
System.out.println("\nbits2 XOR bits1: ");
System.out.println(bits2);
}
}

image-20230804145614093

向量(Vector)

向量(Vector)类和传统数组非常相似,但是 Vector 的大小能根据需要动态的变化。Vector 类实现了一个动态数组。和 ArrayList 很相似,但是两者是不同的:

  • Vector 是同步访问的。
  • Vector 包含了许多传统的方法,这些方法不属于集合框架。

Vector 主要用在事先不知道数组的大小,或者只是需要一个可以改变大小的数组的情况。

序号 方法描述
1 void add(int index, Object element) 在此向量的指定位置插入指定的元素。
2 boolean add(Object o) 将指定元素添加到此向量的末尾。
3 boolean addAll(Collection c) 将指定 Collection 中的所有元素添加到此向量的末尾,按照指定 collection 的迭代器所返回的顺序添加这些元素。
4 boolean addAll(int index, Collection c) 在指定位置将指定 Collection 中的所有元素插入到此向量中。
5 void addElement(Object obj) 将指定的组件添加到此向量的末尾,将其大小增加 1。
6 int capacity() 返回此向量的当前容量。
7 void clear() 从此向量中移除所有元素。
8 Object clone() 返回向量的一个副本。
9 boolean contains(Object elem) 如果此向量包含指定的元素,则返回 true。
10 boolean containsAll(Collection c) 如果此向量包含指定 Collection 中的所有元素,则返回 true。
11 void copyInto(Object[] anArray) 将此向量的组件复制到指定的数组中。
12 Object elementAt(int index) 返回指定索引处的组件。
13 Enumeration elements() 返回此向量的组件的枚举。
14 void ensureCapacity(int minCapacity) 增加此向量的容量(如有必要),以确保其至少能够保存最小容量参数指定的组件数。
15 boolean equals(Object o) 比较指定对象与此向量的相等性。
16 Object firstElement() 返回此向量的第一个组件(位于索引 0) 处的项)。
17 Object get(int index) 返回向量中指定位置的元素。
18 int hashCode() 返回此向量的哈希码值。
19 int indexOf(Object elem) 返回此向量中第一次出现的指定元素的索引,如果此向量不包含该元素,则返回 -1。
20 int indexOf(Object elem, int index) 返回此向量中第一次出现的指定元素的索引,从 index 处正向搜索,如果未找到该元素,则返回 -1。
21 void insertElementAt(Object obj, int index) 将指定对象作为此向量中的组件插入到指定的 index 处。
22 boolean isEmpty() 测试此向量是否不包含组件。
23 Object lastElement() 返回此向量的最后一个组件。
24 int lastIndexOf(Object elem) 返回此向量中最后一次出现的指定元素的索引;如果此向量不包含该元素,则返回 -1。
25 int lastIndexOf(Object elem, int index) 返回此向量中最后一次出现的指定元素的索引,从 index 处逆向搜索,如果未找到该元素,则返回 -1。
26 Object remove(int index) 移除此向量中指定位置的元素。
27 boolean remove(Object o) 移除此向量中指定元素的第一个匹配项,如果向量不包含该元素,则元素保持不变。
28 boolean removeAll(Collection c) 从此向量中移除包含在指定 Collection 中的所有元素。
29 void removeAllElements() 从此向量中移除全部组件,并将其大小设置为零。
30 boolean removeElement(Object obj) 从此向量中移除变量的第一个(索引最小的)匹配项。
31 void removeElementAt(int index) 删除指定索引处的组件。
32 protected void removeRange(int fromIndex, int toIndex) 从此 List 中移除其索引位于 fromIndex(包括)与 toIndex(不包括)之间的所有元素。
33 boolean retainAll(Collection c) 在此向量中仅保留包含在指定 Collection 中的元素。
34 Object set(int index, Object element) 用指定的元素替换此向量中指定位置处的元素。
35 void setElementAt(Object obj, int index) 将此向量指定 index 处的组件设置为指定的对象。
36 void setSize(int newSize) 设置此向量的大小。
37 int size() 返回此向量中的组件数。
38 List subList(int fromIndex, int toIndex) 返回此 List 的部分视图,元素范围为从 fromIndex(包括)到 toIndex(不包括)。
39 Object[] toArray() 返回一个数组,包含此向量中以恰当顺序存放的所有元素。
40 Object[] toArray(Object[] a) 返回一个数组,包含此向量中以恰当顺序存放的所有元素;返回数组的运行时类型为指定数组的类型。
41 String toString() 返回此向量的字符串表示形式,其中包含每个元素的 String 表示形式。
42 void trimToSize() 对此向量的容量进行微调,使其等于向量的当前大小。
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import java.util.*;

public class VectorTest {

public static void main(String args[]) {
// initial size is 3, increment is 2
Vector v = new Vector(3, 2);
System.out.println("Initial size: " + v.size());
System.out.println("Initial capacity: " +
v.capacity());
v.addElement(new Integer(1));
v.addElement(new Integer(2));
v.addElement(new Integer(3));
v.addElement(new Integer(4));
System.out.println("Capacity after four additions: " +
v.capacity());

v.addElement(new Double(5.45));
System.out.println("Current capacity: " +
v.capacity());
v.addElement(new Double(6.08));
v.addElement(new Integer(7));
System.out.println("Current capacity: " +
v.capacity());
v.addElement(new Float(9.4));
v.addElement(new Integer(10));
System.out.println("Current capacity: " +
v.capacity());
v.addElement(new Integer(11));
v.addElement(new Integer(12));
System.out.println("First element: " +
(Integer)v.firstElement());
System.out.println("Last element: " +
(Integer)v.lastElement());
if(v.contains(new Integer(3)))
System.out.println("Vector contains 3.");
// enumerate the elements in the vector.
Enumeration vEnum = v.elements();
System.out.println("\nElements in vector:");
while(vEnum.hasMoreElements())
System.out.print(vEnum.nextElement() + " ");
System.out.println();
}
}

image-20230804145950441

栈(Stack)

栈是 Vector 的一个子类,它实现了一个标准的后进先出的栈。

栈只定义了默认构造函数,用来创建一个空栈。 栈除了包括由 Vector 定义的所有方法,也定义了自己的一些方法。

序号 方法描述
1 boolean empty() 测试堆栈是否为空。
2 Object peek( ) 查看堆栈顶部的对象,但不从堆栈中移除它。
3 Object pop( ) 移除堆栈顶部的对象,并作为此函数的值返回该对象。
4 Object push(Object element) 把项压入堆栈顶部。
5 int search(Object element) 返回对象在堆栈中的位置,以 1 为基数。
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
31
32
33
import java.util.*;

public class StackTest {

static void showpush(Stack<Integer> st, int a) {
st.push(new Integer(a));
System.out.println("push(" + a + ")");
System.out.println("stack: " + st);
}

static void showpop(Stack<Integer> st) {
System.out.print("pop -> ");
Integer a = (Integer) st.pop();
System.out.println(a);
System.out.println("stack: " + st);
}

public static void main(String args[]) {
Stack<Integer> st = new Stack<Integer>();
System.out.println("stack: " + st);
showpush(st, 42);
showpush(st, 66);
showpush(st, 99);
showpop(st);
showpop(st);
showpop(st);
try {
showpop(st);
} catch (EmptyStackException e) {
System.out.println("empty stack");
}
}
}

image-20230804150210386

字典(Dictionary)

字典(Dictionary) 类是一个抽象类,它定义了键映射到值的数据结构。当你想要通过特定的键而不是整数索引来访问数据的时候,这时候应该使用Dictionary。由于 Dictionary 类是抽象类,所以它只提供了键映射到值的数据结构,而没有提供特定的实现。

序号 方法描述
1 Enumeration elements( ) 返回此 dictionary 中值的枚举。
2 Object get(Object key) 返回此 dictionary 中该键所映射到的值。
3 boolean isEmpty( ) 测试此 dictionary 是否不存在从键到值的映射。
4 Enumeration keys( ) 返回此 dictionary 中的键的枚举。
5 Object put(Object key, Object value) 将指定 key 映射到此 dictionary 中指定 value。
6 Object remove(Object key) 从此 dictionary 中移除 key (及其相应的 value)。
7 int size( ) 返回此 dictionary 中条目(不同键)的数量。

Dictionary类已经过时了。在实际开发中,你可以实现Map接口来获取键/值的存储功能。

哈希表(Hashtable)

Hashtable 是原始的 java.util 的一部分, 是一个 Dictionary 具体的实现 。然而,Java2 重构的 Hashtable 实现了 Map 接口,因此,Hashtable 现在集成到了集合框架中。它和 HashMap 类很相似,但是它支持同步。

像 HashMap一样,Hashtable 在哈希表中存储键/值对。当使用一个哈希表,要指定用作键的对象,以及要链接到该键的值。然后,该键经过哈希处理,所得到的散列码被用作存储在该表中值的索引。

序号 方法描述
1 void clear( ) 将此哈希表清空,使其不包含任何键。
2 Object clone( ) 创建此哈希表的浅表副本。
3 boolean contains(Object value) 测试此映射表中是否存在与指定值关联的键。
4 boolean containsKey(Object key) 测试指定对象是否为此哈希表中的键。
5 boolean containsValue(Object value) 如果此 Hashtable 将一个或多个键映射到此值,则返回 true。
6 Enumeration elements( ) 返回此哈希表中的值的枚举。
7 Object get(Object key) 返回指定键所映射到的值,如果此映射不包含此键的映射,则返回 null. 更确切地讲,如果此映射包含满足 (key.equals(k)) 的从键 k 到值 v 的映射,则此方法返回 v;否则,返回 null。
8 boolean isEmpty( ) 测试此哈希表是否没有键映射到值。
9 Enumeration keys( ) 返回此哈希表中的键的枚举。
10 Object put(Object key, Object value) 将指定 key 映射到此哈希表中的指定 value。
11 void rehash( ) 增加此哈希表的容量并在内部对其进行重组,以便更有效地容纳和访问其元素。
12 Object remove(Object key) 从哈希表中移除该键及其相应的值。
13 int size( ) 返回此哈希表中的键的数量。
14 String toString( ) 返回此 Hashtable 对象的字符串表示形式,其形式为 ASCII 字符 “, “ (逗号加空格)分隔开的、括在括号中的一组条目。
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
31
32
import java.util.*;

public class HashTableTest {

public static void main(String args[]) {
// Create a hash map
Hashtable balance = new Hashtable();
Enumeration names;
String str;
double bal;

balance.put("Zara", new Double(3434.34));
balance.put("Mahnaz", new Double(123.22));
balance.put("Ayan", new Double(1378.00));
balance.put("Daisy", new Double(99.22));
balance.put("Qadir", new Double(-19.08));

// Show all balances in hash table.
names = balance.keys();
while(names.hasMoreElements()) {
str = (String) names.nextElement();
System.out.println(str + ": " +
balance.get(str));
}
System.out.println();
// Deposit 1,000 into Zara's account
bal = ((Double)balance.get("Zara")).doubleValue();
balance.put("Zara", new Double(bal+1000));
System.out.println("Zara's new balance: " +
balance.get("Zara"));
}
}

image-20230804150736258

属性(Properties)

Properties 继承于 Hashtable。Properties 类表示了一个持久的属性集,属性列表中每个键及其对应值都是一个字符串。Properties 类被许多 Java 类使用。例如,在获取环境变量时它就作为 System.getProperties() 方法的返回值。

由于 Properties 继承自 Hashtable 类,因此具有 Hashtable 的所有功能,同时还提供了一些特殊的方法来读取和写入属性文件。Properties 类常用于存储程序的配置信息,例如数据库连接信息、日志输出配置、应用程序设置等。使用 Properties 类,可以将这些信息存储在一个文本文件中,并在程序中读取这些信息。

序号 方法描述
1 String getProperty(String key) 用指定的键在此属性列表中搜索属性。
2 String getProperty(String key, String defaultProperty) 用指定的键在属性列表中搜索属性。
3 void list(PrintStream streamOut) 将属性列表输出到指定的输出流。
4 void list(PrintWriter streamOut) 将属性列表输出到指定的输出流。
5 void load(InputStream streamIn) throws IOException 从输入流中读取属性列表(键和元素对)。
6 Enumeration propertyNames( ) 按简单的面向行的格式从输入字符流中读取属性列表(键和元素对)。
7 Object setProperty(String key, String value) 调用 Hashtable 的方法 put。
8 void store(OutputStream streamOut, String description) 以适合使用 load(InputStream)方法加载到 Properties 表中的格式,将此 Properties 表中的属性列表(键和元素对)写入输出流。

Properties 类提供了多种读取和写入属性文件的方法。其中最常用的方法是 load() 和 store() 方法。load() 方法可以从文件中读取属性,而 store() 方法可以将属性写入文件。

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
31
import java.io.*;
import java.util.Properties;

public class PropertiesTest {
public static void main(String[] args) {
Properties prop = new Properties();

try {
// 读取属性文件
prop.load(new FileInputStream("D:\\Learning\\JavaLearn\\Java-First\\chapter2\\src\\config.properties"));

// 获取属性值
String username = prop.getProperty("username");
String password = prop.getProperty("password");

// 输出属性值
System.out.println("username: " + username);
System.out.println("password: " + password);

// 修改属性值
prop.setProperty("username", "newUsername");
prop.setProperty("password", "newPassword");

// 保存属性到文件
prop.store(new FileOutputStream("D:\\Learning\\JavaLearn\\Java-First\\chapter2\\src\\config.properties"), null);

} catch (IOException ex) {
ex.printStackTrace();
}
}
}

image-20230804152649523

2.Java 集合框架

早在 Java2 中之前,Java 就提供了特设类。比如:Dictionary, Vector, Stack, 和 Properties 这些类用来存储和操作对象组。虽然这些类都非常有用,但是它们缺少一个核心的,统一的主题。由于这个原因,使用 Vector 类的方式和使用 Properties 类的方式有着很大不同。

集合框架被设计成要满足以下几个目标。

  • 该框架必须是高性能的。基本集合(动态数组,链表,树,哈希表)的实现也必须是高效的。
  • 该框架允许不同类型的集合,以类似的方式工作,具有高度的互操作性。
  • 对一个集合的扩展和适应必须是简单的。

为此,整个集合框架就围绕一组标准接口而设计。你可以直接使用这些接口的标准实现,诸如: LinkedList, HashSet, 和 TreeSet 等,除此之外你也可以通过这些接口实现自己的集合。

img

从上面的集合框架图可以看到,Java 集合框架主要包括两种类型的容器,一种是集合(Collection),存储一个元素集合,另一种是图(Map),存储键/值对映射。Collection 接口又有 3 种子类型,List、Set 和 Queue,再下面是一些抽象类,最后是具体实现类,常用的有 ArrayList、LinkedList、HashSet、LinkedHashSet、HashMap、LinkedHashMap 等等。

集合框架是一个用来代表和操纵集合的统一架构。所有的集合框架都包含如下内容:

  • 接口:是代表集合的抽象数据类型。例如 Collection、List、Set、Map 等。之所以定义多个接口,是为了以不同的方式操作集合对象。
  • 实现:是集合接口的具体实现。从本质上讲,它们是可重复使用的数据结构,例如:ArrayList、LinkedList、HashSet、HashMap。
  • 算法:是实现集合接口的对象里的方法执行的一些有用的计算,例如:搜索和排序,这些算法实现了多态,那是因为相同的方法可以在相似的接口上有着不同的实现。

除了集合,该框架也定义了几个 Map 接口和类。Map 里存储的是键/值对。尽管 Map 不是集合,但是它们完全整合在集合中。

img

一般遍历数组都是采用for循环或者增强for,这两个方法也可以用在集合框架,但是还有一种方法是采用迭代器遍历集合框架,它是一个对象,实现了 Iterator 接口或 ListIterator 接口。迭代器使你能够通过循环来得到或删除集合的元素。ListIterator 继承了 Iterator,以允许双向遍历列表和修改元素。

遍历 ArrayList

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
import java.util.*;

public class Test{
public static void main(String[] args) {
List<String> list=new ArrayList<String>();
list.add("Hello");
list.add("World");
list.add("HAHAHAHA");
//第一种遍历方法使用 For-Each 遍历 List
for (String str : list) { //也可以改写 for(int i=0;i<list.size();i++) 这种形式
System.out.println(str);
}

//第二种遍历,把链表变为数组相关的内容进行遍历
String[] strArray=new String[list.size()];
list.toArray(strArray);
for(int i=0;i<strArray.length;i++) //这里也可以改写为 for(String str:strArray) 这种形式
{
System.out.println(strArray[i]);
}

//第三种遍历 使用迭代器进行相关遍历
Iterator<String> ite=list.iterator();
while(ite.hasNext())//判断下一个元素之后有值
{
System.out.println(ite.next());
}
}
}

遍历 Map

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
31
32
33
34
35
36
import java.util.*;

public class Test{
public static void main(String[] args) {
Map<String, String> map = new HashMap<String, String>();
map.put("1", "value1");
map.put("2", "value2");
map.put("3", "value3");

//第一种:普遍使用,二次取值
System.out.println("通过Map.keySet遍历key和value:");
for (String key : map.keySet()) {
System.out.println("key= "+ key + " and value= " + map.get(key));
}

//第二种
System.out.println("通过Map.entrySet使用iterator遍历key和value:");
Iterator<Map.Entry<String, String>> it = map.entrySet().iterator();
while (it.hasNext()) {
Map.Entry<String, String> entry = it.next();
System.out.println("key= " + entry.getKey() + " and value= " + entry.getValue());
}

//第三种:推荐,尤其是容量大时
System.out.println("通过Map.entrySet遍历key和value");
for (Map.Entry<String, String> entry : map.entrySet()) {
System.out.println("key= " + entry.getKey() + " and value= " + entry.getValue());
}

//第四种
System.out.println("通过Map.values()遍历所有的value,但不能遍历key");
for (String v : map.values()) {
System.out.println("value= " + v);
}
}
}

3.Java 迭代器

Java迭代器(Iterator)是 Java 集合框架中的一种机制,是一种用于遍历集合(如列表、集合和映射等)的接口。==它提供了一种统一的方式来访问集合中的元素,而不需要了解底层集合的具体实现细节。==

Java Iterator(迭代器)不是一个集合,它是一种用于访问集合的方法,可用于迭代 ArrayList 和 HashSet 等集合。Iterator 是 Java 迭代器最简单的实现,ListIterator 是 Collection API 中的接口, 它扩展了 Iterator 接口。

img

迭代器接口定义了几个方法,最常用的是以下三个:

  • next() - 返回迭代器的下一个元素,并将迭代器的指针移到下一个位置。
  • hasNext() - 用于判断集合中是否还有下一个元素可以访问。
  • remove() - 从集合中删除迭代器最后访问的元素(可选操作)。

注意:使用迭代器遍历集合时,如果在遍历过程中对集合进行了修改(例如添加或删除元素),可能会导致 ConcurrentModificationException 异常,为了避免这个问题,可以使用迭代器自身的 remove() 方法进行删除操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 引入 ArrayList 和 Iterator 类
import java.util.ArrayList;
import java.util.Iterator;

public class IteratorTest {
public static void main(String[] args) {
ArrayList<Integer> numbers = new ArrayList<Integer>();
numbers.add(12);
numbers.add(8);
numbers.add(2);
numbers.add(23);
Iterator<Integer> it = numbers.iterator();
while(it.hasNext()) {
Integer i = it.next();
if(i < 10) {
it.remove(); // 删除小于 10 的元素
}
}
System.out.println(numbers);
}
}

image-20230804163553828

注意:Java Iterator 是一种==单向遍历机制==,即只能从前往后遍历集合中的元素,不能往回遍历。同时,在使用迭代器遍历集合时,不能直接修改集合中的元素,而是需要使用迭代器的 ==remove()== 方法来删除当前元素。

4.Java 泛型

Java 泛型(generics)是 JDK 5 中引入的一个新特性, 泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。==泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。==

泛型方法

你可以写一个泛型方法,该方法在调用时可以接收不同类型的参数。根据传递给泛型方法的参数类型,编译器适当地处理每一个方法调用。下面是定义泛型方法的规则:

  • 所有泛型方法声明都有一个类型参数声明部分(由尖括号分隔),该类型参数声明部分在方法返回类型之前(在下面例子中的 ****)。
  • 每一个类型参数声明部分包含一个或多个类型参数,参数间用逗号隔开。一个泛型参数,也被称为一个类型变量,是用于指定一个泛型类型名称的标识符。
  • 类型参数能被用来声明返回值类型,并且能作为泛型方法得到的实际参数类型的占位符。
  • 泛型方法体的声明和其他方法一样。注意类型参数只能代表引用型类型,不能是原始类型(像 int、double、char 等)。

Java 中泛型标记符:

  • E - Element (在集合中使用,因为集合中存放的是元素)
  • T - Type(Java 类)
  • K - Key(键)
  • V - Value(值)
  • N - Number(数值类型)
  • - 表示不确定的 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
public class GenericMethodTest
{
// 泛型方法 printArray
public static < E > void printArray( E[] inputArray )
{
// 输出数组元素
for ( E element : inputArray ){
System.out.printf( "%s ", element );
}
System.out.println();
}

public static void main( String args[] )
{
// 创建不同类型数组: Integer, Double 和 Character
Integer[] intArray = { 1, 2, 3, 4, 5 };
Double[] doubleArray = { 1.1, 2.2, 3.3, 4.4 };
Character[] charArray = { 'H', 'E', 'L', 'L', 'O' };

System.out.println( "整型数组元素为:" );
printArray( intArray ); // 传递一个整型数组

System.out.println( "\n双精度型数组元素为:" );
printArray( doubleArray ); // 传递一个双精度型数组

System.out.println( "\n字符型数组元素为:" );
printArray( charArray ); // 传递一个字符型数组
}
}

image-20230804172259123

可能有时候,你会想限制那些被允许传递到一个类型参数的类型种类范围。例如,一个操作数字的方法可能只希望接受Number或者Number子类的实例。这就是有界类型参数的目的。

==要声明一个有界的类型参数,首先列出类型参数的名称,后跟extends关键字,最后紧跟它的上界。==

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
public class MaximumTest
{
// 比较三个值并返回最大值
public static <T extends Comparable<T>> T maximum(T x, T y, T z)
{
T max = x; // 假设x是初始最大值
if ( y.compareTo( max ) > 0 ){
max = y; //y 更大
}
if ( z.compareTo( max ) > 0 ){
max = z; // 现在 z 更大
}
return max; // 返回最大对象
}
public static void main( String args[] )
{
System.out.printf( "%d, %d 和 %d 中最大的数为 %d\n\n",
3, 4, 5, maximum( 3, 4, 5 ) );

System.out.printf( "%.1f, %.1f 和 %.1f 中最大的数为 %.1f\n\n",
6.6, 8.8, 7.7, maximum( 6.6, 8.8, 7.7 ) );

System.out.printf( "%s, %s 和 %s 中最大的数为 %s\n","pear",
"apple", "orange", maximum( "pear", "apple", "orange" ) );
}
}

image-20230804172602541

泛型类

泛型类的声明和非泛型类的声明类似,除了在类名后面添加了类型参数声明部分。

和泛型方法一样,泛型类的类型参数声明部分也包含一个或多个类型参数,参数间用逗号隔开。一个泛型参数,也被称为一个类型变量,是用于指定一个泛型类型名称的标识符。因为他们接受一个或多个参数,这些类被称为参数化的类或参数化的类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Box<T> {

private T t;

public void add(T t) {
this.t = t;
}

public T get() {
return t;
}

public static void main(String[] args) {
Box<Integer> integerBox = new Box<Integer>();
Box<String> stringBox = new Box<String>();

integerBox.add(new Integer(10));
stringBox.add(new String("菜鸟教程"));

System.out.printf("整型值为 :%d\n\n", integerBox.get());
System.out.printf("字符串为 :%s\n", stringBox.get());
}
}

image-20230804172733258

类型通配符

(1) 类型通配符一般是使用 ? 代替具体的类型参数。例如 List<?> 在逻辑上是 List<String>,List<Integer> 等所有 List<具体类型实参> 的父类。

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

public class GenericTest {

public static void main(String[] args) {
List<String> name = new ArrayList<String>();
List<Integer> age = new ArrayList<Integer>();
List<Number> number = new ArrayList<Number>();

name.add("icon");
age.add(18);
number.add(314);

getData(name);
getData(age);
getData(number);

}

public static void getData(List<?> data) {
System.out.println("data :" + data.get(0));
}
}

image-20230804172926147

解析: 因为 getData() 方法的参数是 List<?> 类型的,所以 name,age,number 都可以作为这个方法的实参,这就是通配符的作用。

(2) 类型通配符上限通过形如 List<? extends Number> 来定义,如此定义就是通配符泛型值接受Number及其下层子类类型。

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 GenericTest {

public static void main(String[] args) {
List<String> name = new ArrayList<String>();
List<Integer> age = new ArrayList<Integer>();
List<Number> number = new ArrayList<Number>();

name.add("icon");
age.add(18);
number.add(314);

//getUperNumber(name);//1
getUperNumber(age);//2
getUperNumber(number);//3

}

public static void getData(List<?> data) {
System.out.println("data :" + data.get(0));
}

public static void getUperNumber(List<? extends Number> data) {
System.out.println("data :" + data.get(0));
}
}

image-20230804173054017

解析://1 处会出现错误,因为 getUperNumber() 方法中的参数已经限定了参数泛型上限为 Number,所以泛型为 String 是不在这个范围之内,所以会报错。

(3) 类型通配符下限通过形如 List<? super Number> 来定义,表示类型只能接受 Number 及其上层父类类型,如 Object 类型的实例。

(4) 对比extends和super通配符

我们再回顾一下extends通配符。作为方法参数,<? extends T>类型和<? super T>类型的区别在于:

  • <? extends T>允许调用读方法T get()获取T的引用,但不允许调用写方法set(T)传入T的引用(传入null除外);
  • <? super T>允许调用写方法set(T)传入T的引用,但不允许调用读方法T get()获取T的引用(获取Object除外)。

一个是允许读不允许写,另一个是允许写不允许读。

先记住上面的结论,我们来看Java标准库的Collections类定义的copy()方法:

1
2
3
4
5
6
7
8
9
public class Collections {
// 把src的每个元素复制到dest中:
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
for (int i=0; i<src.size(); i++) {
T t = src.get(i);
dest.add(t);
}
}
}

它的作用是把一个List的每个元素依次添加到另一个List中。它的第一个参数是List<? super T>,表示目标List,第二个参数List<? extends T>,表示要复制的List。我们可以简单地用for循环实现复制。在for循环中,我们可以看到,对于类型<? extends T>的变量src,我们可以安全地获取类型T的引用,而对于类型<? super T>的变量dest,我们可以安全地传入T的引用。

这个copy()方法的定义就完美地展示了extendssuper的意图:

  • copy()方法内部不会读取dest,因为不能调用dest.get()来获取T的引用;
  • copy()方法内部也不会修改src,因为不能调用src.add(T)

这是由编译器检查来实现的。如果在方法代码中意外修改了src,或者意外读取了dest,就会导致一个编译错误:

1
2
3
4
5
6
7
8
public class Collections {
// 把src的每个元素复制到dest中:
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
...
T t = dest.get(0); // compile error!
src.add(t); // compile error!
}
}

这个copy()方法的另一个好处是可以安全地把一个List<Integer>添加到List<Number>,但是无法反过来添加:

1
2
3
4
5
6
7
// copy List<Integer> to List<Number> ok:
List<Number> numList = ...;
List<Integer> intList = ...;
Collections.copy(numList, intList);

// ERROR: cannot copy List<Number> to List<Integer>:
Collections.copy(intList, numList);

而这些都是通过superextends通配符,并由编译器强制检查来实现的。

(5) PECS原则

何时使用extends,何时使用super?为了便于记忆,我们可以用PECS原则:Producer Extends Consumer Super。

即:如果需要返回T,它是生产者(Producer),要使用extends通配符;如果需要写入T,它是消费者(Consumer),要使用super通配符。

还是以Collectionscopy()方法为例:

1
2
3
4
5
6
7
8
public class Collections {
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
for (int i=0; i<src.size(); i++) {
T t = src.get(i); // src是producer
dest.add(t); // dest是consumer
}
}
}

需要返回Tsrc是生产者,因此声明为List<? extends T>,需要写入Tdest是消费者,因此声明为List<? super T>

(6) 无限定通配符

我们已经讨论了<? extends T><? super T>作为方法参数的作用。实际上,Java的泛型还允许使用无限定通配符(Unbounded Wildcard Type),即只定义一个?

1
2
void sample(Pair<?> p) {
}

因为<?>通配符既没有extends,也没有super,因此:

  • 不允许调用set(T)方法并传入引用(null除外);
  • 不允许调用T get()方法并获取T引用(只能获取Object引用)。

换句话说,既不能读,也不能写,那只能做一些null判断:

1
2
3
static boolean isNull(Pair<?> p) {
return p.getFirst() == null || p.getLast() == null;
}

大多数情况下,可以引入泛型参数<T>消除<?>通配符:

1
2
3
static <T> boolean isNull(Pair<T> p) {
return p.getFirst() == null || p.getLast() == null;
}

<?>通配符有一个独特的特点,就是:Pair<?>是所有Pair<T>的超类:

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
public static void main(String[] args) {
Pair<Integer> p = new Pair<>(123, 456);
Pair<?> p2 = p; // 安全地向上转型
System.out.println(p2.getFirst() + ", " + p2.getLast());
}

class Pair<T> {
private T first;
private T last;

public Pair(T first, T last) {
this.first = first;
this.last = last;
}

public T getFirst() {
return first;
}
public T getLast() {
return last;
}
public void setFirst(T first) {
this.first = first;
}
public void setLast(T last) {
this.last = last;
}
}

5.Java 序列化

Java 提供了一种对象序列化的机制,该机制中,一个对象可以被表示为一个字节序列,该字节序列包括该对象的数据、有关对象的类型的信息和存储在对象中数据的类型。将序列化对象写入文件之后,可以从文件中读取出来,并且对它进行反序列化,也就是说,对象的类型信息、对象的数据,还有对象中的数据类型可以用来在内存中新建对象。

整个过程都是 Java 虚拟机(JVM)独立的,也就是说,在一个平台上序列化的对象可以在另一个完全不同的平台上反序列化该对象。

ObjectInputStreamObjectOutputStream 是高层次的数据流,它们包含反序列化序列化对象的方法。

1
2
3
4
5
6
7
8
9
10
11
12
public class Employee implements java.io.Serializable
{
public String name;
public String address;
public transient int SSN;
public int number;
public void mailCheck()
{
System.out.println("Mailing a check to " + name
+ " " + address);
}
}

请注意,一个类的对象要想序列化成功,必须满足两个条件:

  1. 该类必须实现 java.io.Serializable 接口。

  2. 该类的所有属性必须是可序列化的。如果有一个属性不是可序列化的,则该属性必须注明是短暂的。

序列化对象

注意: 当序列化一个对象到文件时, 按照 Java 的标准约定是给文件一个 .ser 扩展名。

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
import java.io.*;

public class SerializeTest
{
public static void main(String [] args)
{
Employee e = new Employee();
e.name = "Reyan Ali";
e.address = "Phokka Kuan, Ambehta Peer";
e.SSN = 112233;
e.number = 100;
try
{
FileOutputStream fileOut =
new FileOutputStream("employee.ser");
ObjectOutputStream out = new ObjectOutputStream(fileOut);
out.writeObject(e);
out.close();
fileOut.close();
System.out.printf("Serialized data is saved in employee.ser");
}catch(IOException i)
{
i.printStackTrace();
}
}
}

反序列化对象

下面的 DeserializeTest 程序实例了反序列化,employee.ser 存储了 Employee 对象。

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
31
import java.io.*;

public class DeserializeTest
{
public static void main(String [] args)
{
Employee e = null;
try
{
FileInputStream fileIn = new FileInputStream("employee.ser");
ObjectInputStream in = new ObjectInputStream(fileIn);
e = (Employee) in.readObject();
in.close();
fileIn.close();
}catch(IOException i)
{
i.printStackTrace();
return;
}catch(ClassNotFoundException c)
{
System.out.println("Employee class not found");
c.printStackTrace();
return;
}
System.out.println("Deserialized Employee...");
System.out.println("Name: " + e.name);
System.out.println("Address: " + e.address);
System.out.println("SSN: " + e.SSN);
System.out.println("Number: " + e.number);
}
}

image-20230804194856822

  • readObject() 方法中的 try/catch 代码块尝试捕获 ClassNotFoundException 异常。对于 JVM 可以反序列化对象,它必须是能够找到字节码的类。如果 JVM 在反序列化对象的过程中找不到该类,则抛出一个 ClassNotFoundException 异常。
  • 当对象被序列化时,属性 SSN 的值为 112233,但是因为该属性是 transient 的,该值没有被发送到输出流。所以反序列化后 Employee 对象的 SSN 属性为类型默认值 0。