2023年将会持续于B站、CSDN等各大平台更新,可加入粉丝群与博主交流:838681355,为了老板大G共同努力。
【商务合作请私信或进群联系群主】
六、集合、泛型、枚举
6.1 集合
java
复制代码
Java 提供了集合类。集合类主要负责保存、盛装其他数据,因此集合类也被称为容器类。Java 所有的集合类都位于 java.util 包下,提供了一个表示和操作对象集合的统一构架,包含大量集合接口,以及这些接口的实现类和操作它们的算法。
集合类和数组不一样,数组元素既可以是基本类型的值,也可以是对象(实际上保存的是对象的引用变量),而集合里只能保存对象(实际上只是保存对象的引用变量,但通常习惯上认为集合里保存的是对象)。
集合类就像容器,现实生活中容器的功能,就是添加对象、删除对象、清空容器和判断容器是否为空等,集合类为这些功能都提供了对应的方法。
Java 集合类型分为 Collection 和 Map。
接口名称 |
作 用 |
Iterator 接口 |
集合的输出接口,主要用于遍历输出(即迭代访问)Collection 集合中的元素,Iterator 对象被称之为迭代器。迭代器接口是集合接口的父接口,实现类实现 Collection 时就必须实现 Iterator 接口。 |
Collection 接口 |
是 List、Set 和 Queue 的父接口,是存放一组单值的最大接口。所谓的单值是指集合中的每个元素都是一个对象。一般很少直接使用此接口直接操作。 |
Queue 接口 |
Queue 是 Java 提供的队列实现,有点类似于 List。 |
Dueue 接口 |
是 Queue 的一个子接口,为双向队列。 |
List 接口 |
是最常用的接口。是有序集合,允许有相同的元素。使用 List 能够精确地控制每个元素插入的位置,用户能够使用索引(元素在 List 中的位置,类似于数组下标)来访问 List 中的元素,与数组类似。 |
Set 接口 |
不能包含重复的元素。 |
Map 接口 |
是存放一对值的最大接口,即接口中的每个元素都是一对,以 key➡value 的形式保存。 |
对于 Set、List、Queue 和 Map 这 4 种集合,Java 最常用的实现类分别是 HashSet、TreeSet、ArrayList、ArrayDueue、LinkedList 和 HashMap、TreeMap 等。表 2 介绍了集合中这些常用的实现类。
类名称 |
作用 |
HashSet |
为优化査询速度而设计的 Set。它是基于 HashMap 实现的,HashSet 底层使用 HashMap 来保存所有元素,实现比较简单 |
TreeSet |
实现了 Set 接口,是一个有序的 Set,这样就能从 Set 里面提取一个有序序列 |
ArrayList |
一个用数组实现的 List,能进行快速的随机访问,效率高而且实现了可变大小的数组 |
ArrayDueue |
是一个基于数组实现的双端队列,按"先进先出"的方式操作集合元素 |
LinkedList |
对顺序访问进行了优化,但随机访问的速度相对较慢。此外它还有 addFirst()、addLast()、getFirst()、getLast()、removeFirst() 和 removeLast() 等方法,能把它当成栈(Stack)或队列(Queue)来用 |
HsahMap |
按哈希算法来存取键对象 |
TreeMap |
可以对键对象进行排序 |
6.1.1 Collection接口
java
复制代码
Collection 接口是 List、Set 和 Queue 接口的父接口,通常情况下不被直接使用。Collection 接口定义了一些通用的方法,通过这些方法可以实现对集合的基本操作。定义的方法既可用于操作 Set 集合,也可用于操作 List 和 Queue 集合。
方法名称 |
说明 |
boolean add(E e) |
向集合中添加一个元素,如果集合对象被添加操作改变了,则返回 true。E 是元素的数据类型 |
boolean addAll(Collection c) |
向集合中添加集合 c 中的所有元素,如果集合对象被添加操作改变了,则返回 true。 |
void clear() |
清除集合中的所有元素,将集合长度变为 0。 |
boolean contains(Object o) |
判断集合中是否存在指定元素 |
boolean containsAll(Collection c) |
判断集合中是否包含集合 c 中的所有元素 |
boolean isEmpty() |
判断集合是否为空 |
Iteratoriterator() |
返回一个 Iterator 对象,用于遍历集合中的元素 |
boolean remove(Object o) |
从集合中删除一个指定元素,当集合中包含了一个或多个元素 o 时,该方法只删除第一个符合条件的元素,该方法将返回 true。 |
boolean removeAll(Collection c) |
从集合中删除所有在集合 c 中出现的元素(相当于把调用该方法的集合减去集合 c)。如果该操作改变了调用该方法的集合,则该方法返回 true。 |
boolean retainAll(Collection c) |
从集合中删除集合 c 里不包含的元素(相当于把调用该方法的集合变成该集合和集合 c 的交集),如果该操作改变了调用该方法的集合,则该方法返回 true。 |
int size() |
返回集合中元素的个数 |
Object[] toArray() |
把集合转换为一个数组,所有的集合元素变成对应的数组元素。 |
java
复制代码
示例:
用了 Collection 接口的 ArrayList 实现类来调用 Collection 的方法。add() 方法可以向 Collection 中添加一个元素,而调用 addAll() 方法可以将指定 Collection 中的所有元素添加到另一个 Collection 中。
代码创建了两个集合 list1 和 list2,然后调用 add() 方法向 list1 中添加了两个元素,再调用 addAll() 方法将这两个元素添加到 list2 中。接下来又向 list2 中添加了一个元素,最后输出 list2 集合中的所有元素
如何使用 Collection 接口向集合中添加方法。具体实现代码如下:
public static void main(String[] args) {
ArrayList list1 = new ArrayList(); // 创建集合 list1
ArrayList list2 = new ArrayList(); // 创建集合 list2
list1.add("one"); // 向 list1 添加一个元素
list1.add("two"); // 向 list1 添加一个元素
list2.addAll(list1); // 将 list1 的所有元素添加到 list2
list2.add("three"); // 向 list2 添加一个元素
System.out.println("list2 集合中的元素如下:");
Iterator it1 = list2.iterator();
while (it1.hasNext()) {
System.out.print(it1.next() + "、");
}
}
java
复制代码
示例2:
创建一个案例,演示 Collection 集合中 size()、remove() 和 removeAll() 方法的应用。具体代码如下:
public static void main(String[] args) {
ArrayList list1 = new ArrayList(); // 创建集合 list1
ArrayList list2 = new ArrayList(); // 创建集合 list2
list1.add("one");
list1.add("two");
list1.add("three");
System.out.println("list1 集合中的元素数量:" + list1.size()); // 输出list1中的元素数量
list2.add("two");
list2.add("four");
list2.add("six");
System.out.println("list2 集合中的元素数量:" + list2.size()); // 输出list2中的元素数量
list2.remove(2); // 删除第 3 个元素
System.out.println("\nremoveAll() 方法之后 list2 集合中的元素数量:" + list2.size());
System.out.println("list2 集合中的元素如下:");
Iterator it1 = list2.iterator();
while (it1.hasNext()) {
System.out.print(it1.next() + "、");
}
list1.removeAll(list2);
System.out.println("\nremoveAll() 方法之后 list1 集合中的元素数量:" + list1.size());
System.out.println("list1 集合中的元素如下:");
Iterator it2 = list1.iterator();
while (it2.hasNext()) {
System.out.print(it2.next() + "、");
}
}
6.1.2 List集合
java
复制代码
List 是一个有序、可重复的集合,集合中每个元素都有其对应的顺序索引。List 集合允许使用重复元素,可以通过索引来访问指定位置的集合元素。
List 实现了 Collection 接口,它主要有两个常用的实现类:ArrayList 类和 LinkedList 类。
6.1.2.1 ArrayList类
java
复制代码
ArrayList 类实现了可变数组的大小,存储在内的数据称为元素。它还提供了快速基于索引访问元素的方式,对尾部成员的增加和删除支持较好。使用 ArrayList 创建的集合,允许对集合中的元素进行快速的随机访问,不过,向 ArrayList 中插入与删除元素的速度相对较慢。
ArrayList 类的常用构造方法有如下两种重载形式:
* ArrayList():构造一个初始容量为 10 的空列表。
* ArrayList(Collection<?extends E>c):构造一个包含指定 Collection 元素的列表,这些元素是按照该 Collection 的迭代器返回它们的顺序排列的。
方法名称 |
说明 |
E get(int index) |
获取此集合中指定索引位置的元素,E 为集合中元素的数据类型 |
int index(Object o) |
返回此集合中第一次出现指定元素的索引,如果此集合不包含该元 素,则返回 -1 |
int lastIndexOf(Object o) |
返回此集合中最后一次出现指定元素的索引,如果此集合不包含该 元素,则返回 -1 |
E set(int index, Eelement) |
将此集合中指定索引位置的元素修改为 element 参数指定的对象。 此方法返回此集合中指定索引位置的原元素 |
List subList(int fromlndex, int tolndex) |
返回一个新的集合,新集合中包含 fromlndex 和 tolndex 索引之间 的所有元素。包含 fromlndex 处的元素,不包含 tolndex 索引处的 元素 |
java
复制代码
示例:setter/getter 方法
1)创建一个商品类 Product,在该类中定义 3 个属性和 toString() 方法,分别实现 setter/getter 方法。代码的实现如下:
public class Product {
// 商品类
private int id; // 商品编号
private String name; // 名称
private float price; // 价格
public Product(int id, String name, float price) {
this.name = name;
this.id = id;
this.price = price;
}
// 这里是上面3个属性的setter/getter方法,这里省略
public String toString() {
return "商品编号:" + id + ",名称:" + name + ",价格:" + price;
}
}
2)创建一个测试类,调用 Product 类的构造函数实例化三个对象,并将 Product 对象保存至 ArrayList 集合中。最后遍历该集合,输出商品信息。测试类的代码实现如下:
public class Test {
public static void main(String[] args) {
Product pd1 = new Product(4, "木糖醇", 10);
Product pd2 = new Product(5, "洗发水", 12);
Product pd3 = new Product(3, "热水壶", 49);
List list = new ArrayList(); // 创建集合
list.add(pd1);
list.add(pd2);
list.add(pd3);
System.out.println("*************** 商品信息 ***************");
for (int i = 0; i < list.size(); i++) {
// 循环遍历集合,输出集合元素
Product product = (Product) list.get(i);
System.out.println(product);
}
}
}
java
复制代码
示例二: indexOf() 方法和 lastIndexOf()
创建一个 List 集合 list,然后添加了 7 个元素,由于索引从 0 开始,所以最后一个元素的索引为 6
public static void main(String[] args) {
List list = new ArrayList();
list.add("One");
list.add("|");
list.add("Two");
list.add("|");
list.add("Three");
list.add("|");
list.add("Four");
System.out.println("list 集合中的元素数量:" + list.size());
System.out.println("list 集合中的元素如下:");
Iterator it = list.iterator();
while (it.hasNext()) {
System.out.print(it.next() + "、");
}
System.out.println("\n在 list 集合中'丨'第一次出现的位置是:" + list.indexOf("|"));
System.out.println("在 list 集合中'丨'最后一次出现的位置是:" + list.lastIndexOf("|"));
}
java
复制代码
示例三:subList()
public static void main(String[] args) {
List list = new ArrayList();
list.add("One");
list.add("Two");
list.add("Three");
list.add("Four");
list.add("Five");
list.add("Six");
list.add("Seven");
System.out.println("list 集合中的元素数量:" + list.size());
System.out.println("list 集合中的元素如下:");
Iterator it = list.iterator();
while (it.hasNext()) {
System.out.print(it.next() + "、");
}
List sublist = new ArrayList();
sublist = list.subList(2, 5); // 从list集合中截取索引2~5的元素,保存到sublist集合中
System.out.println("\nsublist 集合中元素数量:" + sublist.size());
System.out.println("sublist 集合中的元素如下:");
it = sublist.iterator();
while (it.hasNext()) {
System.out.print(it.next() + "、");
}
}
6.1.2.2 LinkedList类
java
复制代码
LinkedList 类采用链表结构保存对象,这种结构的优点是便于向集合中插入或者删除元素。
方法名称 |
说明 |
void addFirst(E e) |
将指定元素添加到此集合的开头 |
void addLast(E e) |
将指定元素添加到此集合的末尾 |
E getFirst() |
返回此集合的第一个元素 |
E getLast() |
返回此集合的最后一个元素 |
E removeFirst() |
删除此集合中的第一个元素 |
E removeLast() |
删除此集合中的最后一个元素 |
java
复制代码
示例:
先创建了 5 个 String 对象,分别为 p1、p2、p3、p4 和 p5。同时将 p1、 p2、p3 和 p4 对象使用 add() 方法添加到 LinkedList 集合中,使用 addLast() 方法将 p5 对象添加到 LinkedList 集合中。分别调用 LinkedList 类中的 getFirst() 方法和 getLast() 方法获取第一个和最后一个商品名称。最后使用 removeLast() 方法将最后一个商品信息删除,并将剩余商品信息打印出来。
LinkedList<String> 中的 <String> 是 Java 中的泛型,用于指定集合中元素的数据类型,例如这里指定元素类型为 String,则该集合中不能添加非 String 类型的元素。
public class Test {
public static void main(String[] args) {
LinkedList<String> products = new LinkedList<String>(); // 创建集合对象
String p1 = new String("六角螺母");
String p2 = new String("10A 电缆线");
String p3 = new String("5M 卷尺");
String p4 = new String("4CM 原木方板");
products.add(p1); // 将 p1 对象添加到 LinkedList 集合中
products.add(p2); // 将 p2 对象添加到 LinkedList 集合中
products.add(p3); // 将 p3 对象添加到 LinkedList 集合中
products.add(p4); // 将 p4 对象添加到 LinkedList 集合中
String p5 = new String("标准文件夹小柜");
products.addLast(p5); // 向集合的末尾添加p5对象
System.out.print("*************** 商品信息 ***************");
System.out.println("\n目前商品有:");
for (int i = 0; i < products.size(); i++) {
System.out.print(products.get(i) + "\t");
}
System.out.println("\n第一个商品的名称为:" + products.getFirst());
System.out.println("最后一个商品的名称为:" + products.getLast());
products.removeLast(); // 删除最后一个元素
System.out.println("删除最后的元素,目前商品有:");
for (int i = 0; i < products.size(); i++) {
System.out.print(products.get(i) + "\t");
}
}
}
6.1.2.3 ArrayList类和LinkedList类区别
java
复制代码
* ArrayList 与 LinkedList 都是 List 接口的实现类,因此都实现了 List 的所有未实现的方法,只是实现的方式有所不同。
* ArrayList 是基于动态数组数据结构的实现,访问元素速度优于 LinkedList。LinkedList 是基于链表数据结构的实现,占用的内存空间比较大,但在批量插入或删除数据时优于 ArrayList。
* 对于快速访问对象的需求,使用 ArrayList 实现执行效率上会比较好。需要频繁向集合中插入和删除元素时,使用 LinkedList 类比 ArrayList 类效果高。
6.1.3 Set集合
java
复制代码
Set 集合类似于一个罐子,程序可以依次把多个对象"丢进"Set 集合,而 Set 集合通常不能记住元素的添加顺序。也就是说 Set 集合中的对象不按特定的方式排序,只是简单地把对象加入集合。Set 集合中不能包含重复的对象,并且最多只允许包含一个 null 元素。
Set 实现了 Collection 接口,它主要有两个常用的实现类:HashSet 类和 TreeSet类。
6.1.3.1 HashSet类
java
复制代码
HashSet 是 Set 接口的典型实现,大多数时候使用 Set 集合时就是使用这个实现类。HashSet 是按照 Hash 算法来存储集合中的元素。因此具有很好的存取和查找性能。
HashSet 具有以下特点:
1. 不能保证元素的排列顺序,顺序可能与添加顺序不同,顺序也有可能发生变化。
2. HashSet 不是同步的,如果多个线程同时访问或修改一个 HashSet,则必须通过代码来保证其同步。
3. 集合元素值可以是 null。
在 HashSet 类中实现了 Collection 接口中的所有方法。HashSet 类的常用构造方法重载形式如下。
1. HashSet():构造一个新的空的 Set 集合。
2. HashSet(Collection<? extends E>c):构造一个包含指定 Collection 集合元素的新 Set 集合。其中,"< >"中的 extends 表示 HashSet 的父类,即指明该 Set 集合中存放的集合元素类型。c 表示其中的元素将被存放在此 Set 集合中。
下面的代码演示了创建两种不同形式的 HashSet 对象。
* HashSet hs = new HashSet(); // 调用无参的构造函数创建HashSet对象
* HashSet<String> hss = new HashSet<String>(); // 创建泛型的 HashSet 集合对象
java
复制代码
示例:
使用 HashSet 创建一个 Set 集合,并向该集合中添加 4 套教程。具体实现代码如下:
首先使用 HashSet 类的构造方法创建了一个 Set 集合,接着创建了 4 个 String 类型的对象,并将这些对象存储到 Set 集合中。使用 HashSet 类中的 iterator() 方法获取一个 Iterator 对象,并调用其 hasNext() 方法遍历集合元素,再将使用 next() 方法读取的元素强制转换为 String 类型。最后调用 HashSet 类中的 size() 方法获取集合元素个数。
public static void main(String[] args) {
HashSet<String> courseSet = new HashSet<String>(); // 创建一个空的 Set 集合
String course1 = new String("Java入门教程");
String course2 = new String("Python基础教程");
String course3 = new String("C语言学习教程");
String course4 = new String("Golang入门教程");
courseSet.add(course1); // 将 course1 存储到 Set 集合中
courseSet.add(course2); // 将 course2 存储到 Set 集合中
courseSet.add(course3); // 将 course3 存储到 Set 集合中
courseSet.add(course4); // 将 course4 存储到 Set 集合中
System.out.println("C语言中文网教程有:");
Iterator<String> it = courseSet.iterator();
while (it.hasNext()) {
System.out.println("《" + (String) it.next() + "》"); // 输出 Set 集合中的元素
}
System.out.println("有" + courseSet.size() + "套精彩教程!");
}
6.1.3.2 TreeSet类
java
复制代码
TreeSet 类同时实现了 Set 接口和 SortedSet 接口。SortedSet 接口是 Set 接口的子接口,可以实现对集合进行自然排序。
TreeSet 只能对实现了 Comparable 接口的类对象进行排序。
类 |
比较方式 |
包装类(BigDecimal、Biglnteger、 Byte、Double、 Float、Integer、Long 及 Short) |
按数字大小比较 |
Character |
按字符的 Unicode 值的数字大小比较 |
String |
按字符串中字符的 Unicode 值的数字大小比较 |
方法名称 |
说明 |
E first() |
返回此集合中的第一个元素。其中,E 表示集合中元素的数据类型 |
E last() |
返回此集合中的最后一个元素 |
E poolFirst() |
获取并移除此集合中的第一个元素 |
E poolLast() |
获取并移除此集合中的最后一个元素 |
SortedSet subSet(E fromElement,E toElement) |
返回一个新的集合,新集合包含原集合中 fromElement 对象与 toElement 对象之间的所有对象。包含 fromElement 对象,不包含 toElement 对象 |
SortedSet headSet<E toElement〉 |
返回一个新的集合,新集合包含原集合中 toElement 对象之前的所有对象。 不包含 toElement 对象 |
SortedSet tailSet(E fromElement) |
返回一个新的集合,新集合包含原集合中 fromElement 对象之后的所有对 象。包含 fromElement 对象 |
java
复制代码
示例:
使用 TreeSet 类来创建 Set 集合,完成学生成绩查询功能。
创建一个 TreeSet 集合对象 scores,并向该集合中添加 5 个 Double 对象。接着使用 while 循环遍历 scores 集合对象,输出该对象中的元素,然后调用 TreeSet 类中的 contains() 方法获取该集合中是否存在指定的元素。最后分别调用 TreeSet 类中的 headSet() 方法和 tailSet() 方法获取不及格的成绩和 90 分以上的成绩。
public class Test08 {
public static void main(String[] args) {
TreeSet<Double> scores = new TreeSet<Double>(); // 创建 TreeSet 集合
Scanner input = new Scanner(System.in);
System.out.println("------------学生成绩管理系统-------------");
for (int i = 0; i < 5; i++) {
System.out.println("第" + (i + 1) + "个学生成绩:");
double score = input.nextDouble();
// 将学生成绩转换为Double类型,添加到TreeSet集合中
scores.add(Double.valueOf(score));
}
Iterator<Double> it = scores.iterator(); // 创建 Iterator 对象
System.out.println("学生成绩从低到高的排序为:");
while (it.hasNext()) {
System.out.print(it.next() + "\t");
}
System.out.println("\n请输入要查询的成绩:");
double searchScore = input.nextDouble();
if (scores.contains(searchScore)) {
System.out.println("成绩为: " + searchScore + " 的学生存在!");
} else {
System.out.println("成绩为: " + searchScore + " 的学生不存在!");
}
// 查询不及格的学生成绩
SortedSet<Double> score1 = scores.headSet(60.0);
System.out.println("\n不及格的成绩有:");
for (int i = 0; i < score1.toArray().length; i++) {
System.out.print(score1.toArray()[i] + "\t");
}
// 查询90分以上的学生成绩
SortedSet<Double> score2 = scores.tailSet(90.0);
System.out.println("\n90 分以上的成绩有:");
for (int i = 0; i < score2.toArray().length; i++) {
System.out.print(score2.toArray()[i] + "\t");
}
}
}
6.1.4 Map集合
java
复制代码
Map 是一种键-值对(key-value)集合,Map 集合中的每一个元素都包含一个键(key)对象和一个值(value)对象。用于保存具有映射关系的数据。
Map 接口主要有两个实现类:HashMap 类和 TreeMap 类。其中,HashMap 类按哈希算法来存取键对象,而 TreeMap 类可以对键对象进行排序。
void clear() |
删除该 Map 对象中的所有 key-value 对。 |
boolean containsKey(Object key) |
查询 Map 中是否包含指定的 key,如果包含则返回 true。 |
boolean containsValue(Object value) |
查询 Map 中是否包含一个或多个 value,如果包含则返回 true。 |
V get(Object key) |
返回 Map 集合中指定键对象所对应的值。V 表示值的数据类型 |
V put(K key, V value) |
向 Map 集合中添加键-值对,如果当前 Map 中已有一个与该 key 相等的 key-value 对,则新的 key-value 对会覆盖原来的 key-value 对。 |
void putAll(Map m) |
将指定 Map 中的 key-value 对复制到本 Map 中。 |
V remove(Object key) |
从 Map 集合中删除 key 对应的键-值对,返回 key 对应的 value,如果该 key 不存在,则返回 null |
boolean remove(Object key, Object value) |
这是 Java 8 新增的方法,删除指定 key、value 所对应的 key-value 对。如果从该 Map 中成功地删除该 key-value 对,该方法返回 true,否则返回 false。 |
Set entrySet() |
返回 Map 集合中所有键-值对的 Set 集合,此 Set 集合中元素的数据类型为 Map.Entry |
Set keySet() |
返回 Map 集合中所有键对象的 Set 集合 |
boolean isEmpty() |
查询该 Map 是否为空(即不包含任何 key-value 对),如果为空则返回 true。 |
int size() |
返回该 Map 里 key-value 对的个数 |
Collection values() |
返回该 Map 里所有 value 组成的 Collection |
java
复制代码
Map 集合最典型的用法就是成对地添加、删除 key-value 对,接下来即可判断该 Map 中是否包含指定 key,也可以通过 Map 提供的 keySet() 方法获取所有 key 组成的集合,进而遍历 Map 中所有的 key-value 对。
使用 HashMap 来存储学生信息,其键为学生学号,值为姓名。毕业时,需要用户输入学生的学号,并根据学号进行删除操作。
public class Test09 {
public static void main(String[] args) {
HashMap users = new HashMap();
users.put("11", "张浩太"); // 将学生信息键值对存储到Map中
users.put("22", "刘思诚");
users.put("33", "王强文");
users.put("44", "李国量");
users.put("55", "王路路");
System.out.println("******** 学生列表 ********");
Iterator it = users.keySet().iterator();
while (it.hasNext()) {
// 遍历 Map
Object key = it.next();
Object val = users.get(key);
System.out.println("学号:" + key + ",姓名:" + val);
}
Scanner input = new Scanner(System.in);
System.out.println("请输入要删除的学号:");
int num = input.nextInt();
if (users.containsKey(String.valueOf(num))) { // 判断是否包含指定键
users.remove(String.valueOf(num)); // 如果包含就删除
} else {
System.out.println("该学生不存在!");
}
System.out.println("******** 学生列表 ********");
it = users.keySet().iterator();
while (it.hasNext()) {
Object key = it.next();
Object val = users.get(key);
System.out.println("学号:" + key + ",姓名:" + val);
}
}
}
6.1.4.1 遍历Map集合
java
复制代码
Map 集合的遍历与 List 和 Set 集合不同。Map 有两组值,因此遍历时可以只遍历值的集合,也可以只遍历键的集合,也可以同时遍历。Map 以及实现 Map 的接口类(如 HashMap、TreeMap、LinkedHashMap、Hashtable 等)都可以用以下几种方式遍历。
6.1.4.2 for循环遍历
java
复制代码
在 for 循环中使用 entries 实现 Map 的遍历(最常见和最常用的)。
public static void main(String[] args) {
Map<String, String> map = new HashMap<String, String>();
map.put("Java入门教程", "http://c.biancheng.net/java/");
map.put("C语言入门教程", "http://c.biancheng.net/c/");
for (Map.Entry<String, String> entry : map.entrySet()) {
String mapKey = entry.getKey();
String mapValue = entry.getValue();
System.out.println(mapKey + ":" + mapValue);
}
}
6.1.4.3 for-each循环遍历
java
复制代码
使用 for-each 循环遍历 key 或者 values,一般适用于只需要 Map 中的 key 或者 value 时使用。性能上比 entrySet 较好。
Map<String, String> map = new HashMap<String, String>();
map.put("Java入门教程", "http://c.biancheng.net/java/");
map.put("C语言入门教程", "http://c.biancheng.net/c/");
// 打印键集合
for (String key : map.keySet()) {
System.out.println(key);
}
// 打印值集合
for (String value : map.values()) {
System.out.println(value);
}
6.1.4.4 迭代器(Iterator)遍历
java
复制代码
Map<String, String> map = new HashMap<String, String>();
map.put("Java入门教程", "http://c.biancheng.net/java/");
map.put("C语言入门教程", "http://c.biancheng.net/c/");
Iterator<Entry<String, String>> entries = map.entrySet().iterator();
while (entries.hasNext()) {
Entry<String, String> entry = entries.next();
String key = entry.getKey();
String value = entry.getValue();
System.out.println(key + ":" + value);
}
6.1.4.5 键值遍历
java
复制代码
通过键找值遍历,这种方式的效率比较低,因为本身从键取值是耗时的操作。
for(String key : map.keySet()){
String value = map.get(key);
System.out.println(key+":"+value);
}
6.2 Collections类
java
复制代码
Collections 类是 Java 提供的一个操作 Set、List 和 Map 等集合的工具类。Collections 类提供了许多操作集合的静态方法,借助这些静态方法可以实现集合元素的排序、查找替换和复制等操作。
6.2.1 排序(正向与逆向)
java
复制代码
Collections 提供了如下方法用于对 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 个元素"整体"移到后面。该方法不会改变集合的长度。
java
复制代码
示例:正向
编写一个程序,对用户输入的 5 个商品价格进行排序后输出。这里要求使用 Collections 类中 sort() 方法按从低到高的顺序对其进行排序,最后将排序后的成绩输出。
public class Test1 {
public static void main(String[] args) {
Scanner input = new Scanner(System.in);
List prices = new ArrayList();
for (int i = 0; i < 5; i++) {
System.out.println("请输入第 " + (i + 1) + " 个商品的价格:");
int p = input.nextInt();
prices.add(Integer.valueOf(p)); // 将录入的价格保存到List集合中
}
Collections.sort(prices); // 调用sort()方法对集合进行排序
System.out.println("价格从低到高的排列为:");
for (int i = 0; i < prices.size(); i++) {
System.out.print(prices.get(i) + "\t");
}
}
}
java
复制代码
示例:反向
使用 Collections 类的 reverse() 方法对保存到 List 集合中的 5 个商品名称进行反转排序,并输出排序后的商品信息。
public class Test2 {
public static void main(String[] args) {
Scanner input = new Scanner(System.in);
List students = new ArrayList();
System.out.println("******** 商品信息 ********");
for (int i = 0; i < 5; i++) {
System.out.println("请输入第 " + (i + 1) + " 个商品的名称:");
String name = input.next();
students.add(name); // 将录入的商品名称存到List集合中
}
Collections.reverse(students); // 调用reverse()方法对集合元素进行反转排序
System.out.println("按录入时间的先后顺序进行降序排列为:");
for (int i = 0; i < 5; i++) {
System.out.print(students.get(i) + "\t");
}
}
}
6.2.2 查找、替换操作
java
复制代码
Collections 还提供了如下常用的用于查找、替换集合元素的方法。
* 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 oldVal, Object newVal):使用一个新值 newVal 替换 List 对象的所有旧值 oldVal。
6.2.2.1 fil()方法
java
复制代码
编写一个程序,要求用户输入 3 个商品名称,然后使用 Collections 类中的 fill() 方法对商品信息进行重置操作,即将所有名称都更改为"未填写":
public class Test3 {
public static void main(String[] args) {
Scanner input = new Scanner(System.in);
List products = new ArrayList();
System.out.println("******** 商品信息 ********");
for (int i = 0; i < 3; i++) {
System.out.println("请输入第 " + (i + 1) + " 个商品的名称:");
String name = input.next();
products.add(name); // 将用户录入的商品名称保存到List集合中
}
System.out.println("重置商品信息,将所有名称都更改为'未填写'");
Collections.fill(products, "未填写");
System.out.println("重置后的商品信息为:");
for (int i = 0; i < products.size(); i++) {
System.out.print(products.get(i) + "\t");
}
}
}
6.2.2.2 replaceAll()方法
java
复制代码
向 List 集合中添加 4 个数据,然后调用 Collections 类中的 max() 和 min() 方法输出集合中的最大最小元素,replaceAll() 替换元素,frequency() 判断指定数据在 List 集合中出现的次数,最后用 binarySearch() 进行二分法查询。
public class Test4 {
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));
}
}
6.2.3 复制
java
复制代码
Collections 类的 copy() 静态方法用于将指定集合中的所有元素复制到另一个集合中。执行 copy() 方法后,目标集合中每个已复制元素的索引将等同于源集合中该元素的索引。
copy() 方法的语法格式如下:
void copy(List <? super T> dest,List<? extends T> src)
java
复制代码
示例:
在一个集合中保存了 5 个商品名称,现在要使用 Collections 类中的 copy() 方法将其中的 3 个替换掉。具体实现的代码如下:
public class Test5 {
public static void main(String[] args) {
Scanner input = new Scanner(System.in);
List srcList = new ArrayList();
List destList = new ArrayList();
destList.add("苏打水");
destList.add("木糖醇");
destList.add("方便面");
destList.add("火腿肠");
destList.add("冰红茶");
System.out.println("原有商品如下:");
for (int i = 0; i < destList.size(); i++) {
System.out.println(destList.get(i));
}
System.out.println("输入替换的商品名称:");
for (int i = 0; i < 3; i++) {
System.out.println("第 " + (i + 1) + " 个商品:");
String name = input.next();
srcList.add(name);
}
// 调用copy()方法将当前商品信息复制到原有商品信息集合中
Collections.copy(destList, srcList);
System.out.println("当前商品有:");
for (int i = 0; i < destList.size(); i++) {
System.out.print(destList.get(i) + "\t");
}
}
}
如上述代码,首先创建了两个 List 对象 srcList 和 destList,并向 destList 集合中添加了 5 个元素,向 srcList 集合中添加了 3 个元素,然后调用 Collections 类中 copy() 方法将 srcList 集合中的全部元素复制到 destList 集合中。由于 destList 集合中含有 5 个元素,故最后两个元素不会被覆盖。
6.3 Lambda表达式遍历Collection集合
java
复制代码
Java 8 为 Iterable 接口新增了一个 forEach(Consumer action) 默认方法,该方法所需参数的类型是一个函数式接口,而 Iterable 接口是 Collection 接口的父接口,因此 Collection 集合也可直接调用该方法。
当程序调用 Iterable 的 forEach(Consumer action) 遍历集合元素时,程序会依次将集合元素传给 Consumer 的 accept(T t) 方法(该接口中唯一的抽象方法)。正因为 Consumer 是函数式接口,因此可以使用 Lambda 表达式来遍历集合元素。
如下程序示范了使用 Lambda 表达式来遍历集合元素。
public class CollectionEach {
public static void main(String[] args) {
// 创建一个集合
Collection objs = new HashSet();
objs.add("C语言中文网Java教程");
objs.add("C语言中文网C语言教程");
objs.add("C语言中文网C++教程");
// 调用forEach()方法遍历集合
objs.forEach(obj -> System.out.println("迭代集合元素:" + obj));
}
}
6.3.1 迭代器遍历集合元素
java
复制代码
Iterator(迭代器)是一个接口,它的作用就是遍历容器的所有元素,也是 Java 集合框架的成员,但它与 Collection 和 Map 系列的集合不一样,Collection 和 Map 系列集合主要用于盛装其他对象,而 Iterator 则主要用于遍历(即迭代访问)Collection 集合中的元素。
Iterator 接口隐藏了各种 Collection 实现类的底层细节,向应用程序提供了遍历 Collection 集合元素的统一编程接口。Iterator 接口里定义了如下 4 个方法:
1. boolean hasNext():如果被迭代的集合元素还没有被遍历完,则返回 true。
2. Object next():返回集合里的下一个元素。
3. void remove():删除集合里上一次 next 方法返回的元素。
4. void forEachRemaining(Consumer action):这是 Java 8 为 Iterator 新增的默认方法,该方法可使用 Lambda 表达式来遍历集合元素。
java
复制代码
示例:通过 Iterator 接口来遍历集合元素
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
public class IteratorTest {
public static void main(String[] args) {
// 创建一个集合
Collection objs = new HashSet();
objs.add("C语言中文网Java教程");
objs.add("C语言中文网C语言教程");
objs.add("C语言中文网C++教程");
// 调用forEach()方法遍历集合
// 获取books集合对应的迭代器
Iterator it = objs.iterator();
while (it.hasNext()) {
// it.next()方法返回的数据类型是Object类型,因此需要强制类型转换
String obj = (String) it.next();
System.out.println(obj);
if (obj.equals("C语言中文网C语言教程")) {
// 从集合中删除上一次next()方法返回的元素
it.remove();
}
// 对book变量赋值,不会改变集合元素本身
obj = "C语言中文网Python语言教程";
}
System.out.println(objs);
}
}
6.3.2 Lambda表达式遍历迭代器
java
复制代码
Java 8 为 Iterator 引入了一个 forEachRemaining(Consumer action) 默认方法,该方法所需的 Consumer 参数同样也是函数式接口。当程序调用 Iterator 的 forEachRemaining(Consumer action) 遍历集合元素时,程序会依次将集合元素传给 Consumer 的 accept(T t) 方法(该接口中唯一的抽象方法)。
java.util.function 中的 Function、Supplier、Consumer、Predicate 和其他函数式接口被广泛用在支持 Lambda 表达式的 API 中。"void accept(T t);"是 Consumer 的核心方法,用来对给定的参数 T 执行定义操作。
如下程序示范了使用 Lambda 表达式来遍历集合元素。
public class IteratorEach {
public static void main(String[] args) {
// 创建一个集合
Collection objs = new HashSet();
objs.add("C语言中文网Java教程");
objs.add("C语言中文网C语言教程");
objs.add("C语言中文网C++教程");
// 获取objs集合对应的迭代器
Iterator it = objs.iterator();
// 使用Lambda表达式(目标类型是Comsumer)来遍历集合元素
it.forEachRemaining(obj -> System.out.println("迭代集合元素:" + obj));
}
}
6.3.3 for-reach遍历Collection集合
java
复制代码
使用 foreach 循环来迭代访问集合元素。
public class ForeachTest {
public static void main(String[] args) {
// 创建一个集合
Collection objs = new HashSet();
objs.add("C语言中文网Java教程");
objs.add("C语言中文网C语言教程");
objs.add("C语言中文网C++教程");
for (Object obj : objs) {
// 此处的obj变量也不是集合元素本身
String obj1 = (String) obj;
System.out.println(obj1);
if (obj1.equals("C语言中文网Java教程")) {
// 下面代码会引发 ConcurrentModificationException 异常
objs.remove(obj);
}
}
System.out.println(objs);
}
}
6.4 Predicate操作Collection集合
java
复制代码
Java 8 起为 Collection 集合新增了一个 removeIf(Predicate filter) 方法,该方法将会批量删除符合 filter 条件的所有元素。该方法需要一个 Predicate 对象作为参数,Predicate 也是函数式接口,因此可使用 Lambda 表达式作为参数。
示范了使用 Predicate 来过滤集合。
public class ForeachTest {
public static void main(String[] args) {
// 创建一个集合
Collection objs = new HashSet();
objs.add(new String("C语言中文网Java教程"));
objs.add(new String("C语言中文网C++教程"));
objs.add(new String("C语言中文网C语言教程"));
objs.add(new String("C语言中文网Python教程"));
objs.add(new String("C语言中文网Go教程"));
// 使用Lambda表达式(目标类型是Predicate)过滤集合
objs.removeIf(ele -> ((String) ele).length() < 12);
System.out.println(objs);
}
}
6.5 Stream操作Collection集合
java
复制代码
Java 8 还新增了 Stream、IntStream、LongStream、DoubleStream 等流式 API,这些 API 代表多个支持串行和并行聚集操作的元素。上面 4 个接口中,Stream 是一个通用的流接口,而 IntStream、LongStream、 DoubleStream 则代表元素类型为 int、long、double 的流。
独立使用 Stream 的步骤如下:
1. 使用 Stream 或 XxxStream 的 builder() 类方法创建该 Stream 对应的 Builder。
2. 重复调用 Builder 的 add() 方法向该流中添加多个元素。
3. 调用 Builder 的 build() 方法获取对应的 Stream。
4. 调用 Stream 的聚集方法。
Stream 提供了大量的方法进行聚集操作,这些方法既可以是"中间的"(intermediate),也可以是 "末端的"(terminal)。
* 中间方法:中间操作允许流保持打开状态,并允许直接调用后续方法。上面程序中的 map() 方法就是中间方法。中间方法的返回值是另外一个流。
* 末端方法:末端方法是对流的最终操作。当对某个 Stream 执行末端方法后,该流将会被"消耗"且不再可用。上面程序中的 sum()、count()、average() 等方法都是末端方法。
除此之外,关于流的方法还有如下两个特征。
* 有状态的方法:这种方法会给流增加一些新的属性,比如元素的唯一性、元素的最大数量、保证元素以排序的方式被处理等。有状态的方法往往需要更大的性能开销。
* 短路方法:短路方法可以尽早结束对流的操作,不必检查所有的元素。
java
复制代码
示例:
对于大部分聚集方法而言,每个 Stream 只能执行一次。例如如下程序。
创建了一个 IntStream,接下来分别多次调用 IntStream 的聚集方法执行操作,这样即可获取该流的相关信息。注意:上面 5~13 行代码每次只能执行一行,因此需要把其他代码注释掉。
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);
// 使用方法引用的方式来遍历集合元素
newIs.forEach(System.out::println); // 输岀 41 27 -3 37
}
}
方法 |
说明 |
filter(Predicate predicate) |
过滤 Stream 中所有不符合 predicate 的元素 |
mapToXxx(ToXxxFunction mapper) |
使用 ToXxxFunction 对流中的元素执行一对一的转换,该方法返回的新流中包含了 ToXxxFunction 转换生成的所有元素。 |
peek(Consumer action) |
依次对每个元素执行一些操作,该方法返回的流与原有流包含相同的元素。该方法主要用于调试。 |
distinct() |
该方法用于排序流中所有重复的元素(判断元素重复的标准是使用 equals() 比较返回 true)。这是一个有状态的方法。 |
sorted() |
该方法用于保证流中的元素在后续的访问中处于有序状态。这是一个有状态的方法。 |
limit(long maxSize) |
该方法用于保证对该流的后续访问中最大允许访问的元素个数。这是一个有状态的、短路方法。 |
方法 |
说明 |
forEach(Consumer action) |
遍历流中所有元素,对每个元素执行action |
toArray() |
将流中所有元素转换为一个数组 |
reduce() |
该方法有三个重载的版本,都用于通过某种操作来合并流中的元素 |
min() |
返回流中所有元素的最小值 |
max() |
返回流中所有元素的最大值 |
count() |
返回流中所有元素的数量 |
anyMatch(Predicate predicate) |
判断流中是否至少包含一个元素符合 Predicate 条件。 |
allMatch(Predicate predicate) |
判断流中是否每个元素都符合 Predicate 条件 |
noneMatch(Predicate predicate) |
判断流中是否所有元素都不符合 Predicate 条件 |
findFirst() |
返回流中的第一个元素 |
findAny() |
返回流中的任意一个元素 |
6.6 JAVA9新增不可变集合
java
复制代码
Java 9 版本以前,假如要创建一个包含 6 个元素的 Set 集合,程序需要先创建 Set 集合,然后调用 6 次 add() 方法向 Set 集合中添加元素。Java 9 对此进行了简化,程序直接调用 Set、List、Map 的 of() 方法即可创建包含 N 个元素的不可变集合,这样一行代码就可创建包含 N 个元素的集合。
不可变意味着程序不能向集合中添加元素,也不能从集合中删除元素。
java
复制代码
示例:
如何创建不可变集合。
public class Java9Collection {
public static void main(String[] args) {
// 创建包含4个元素的Set集合
Set set = Set.of("Java", "Kotlin", "Go", "Swift");
System.out.println(set);
// 不可变集合,下面代码导致运行时错误
// set.add("Ruby");
// 创建包含4个元素的List集合
List list = List.of(34, -25, 67, 231);
System.out.println(list);
// 不可变集合,下面代码导致运行时错误
// list.remove(1);
// 创建包含3个key-value对的Map集合
Map map = Map.of("语文", 89, "数学", 82, "英语", 92);
System.out.println(map);
// 不可变集合,下面代码导致运行时错误
// map.remove("语文");
// 使用Map.entry()方法显式构建key-value对
Map map2 = Map.ofEntries(Map.entry("语文", 89), Map.entry("数学", 82), Map.entry("英语", 92));
System.out.println(map2);
}
}
6.7 泛型
java
复制代码
泛型可以在编译的时候检查类型安全,并且所有的强制转换都是自动和隐式的,提高了代码的重用率。
6.7.1 泛型集合
java
复制代码
泛型本质上是提供类型的"类型参数",也就是参数化类型。
1)首先需要创建一个表示图书的实体类 Book,其中包括的图书信息有图书编号、图书名称和价格。Book 类的具体代码如下:
public class Book {
private int Id; // 图书编号
private String Name; // 图书名称
private int Price; // 图书价格
public Book(int id, String name, int price) { // 构造方法
this.Id = id;
this.Name = name;
this.Price = price;
}
public String toString() { // 重写 toString()方法
return this.Id + ", " + this.Name + "," + this.Price;
}
}
2)使用 Book 作为类型创建 Map 和 List 两个泛型集合,然后向集合中添加图书元素,最后输出集合中的内容。具体代码如下:
public class Test14 {
public static void main(String[] args) {
// 创建3个Book对象
Book book1 = new Book(1, "唐诗三百首", 8);
Book book2 = new Book(2, "小星星", 12);
Book book3 = new Book(3, "成语大全", 22);
Map<Integer, Book> books = new HashMap<Integer, Book>(); // 定义泛型 Map 集合
books.put(1001, book1); // 将第一个 Book 对象存储到 Map 中
books.put(1002, book2); // 将第二个 Book 对象存储到 Map 中
books.put(1003, book3); // 将第三个 Book 对象存储到 Map 中
System.out.println("泛型Map存储的图书信息如下:");
for (Integer id : books.keySet()) {
// 遍历键
System.out.print(id + "------");
System.out.println(books.get(id)); // 不需要类型转换
}
List<Book> bookList = new ArrayList<Book>(); // 定义泛型的 List 集合
bookList.add(book1);
bookList.add(book2);
bookList.add(book3);
System.out.println("泛型List存储的图书信息如下:");
for (int i = 0; i < bookList.size(); i++) {
System.out.println(bookList.get(i)); // 这里不需要类型转换
}
}
}
6.7.2 泛型类
java
复制代码
除了可以定义泛型集合之外,还可以直接限定泛型类的类型参数。语法格式如下:
public class class_name<data_type1,data_type2,...>{}
其中,class_name 表示类的名称,data_ type1 等表示类型参数。Java 泛型支持声明一个以上的类型参数,只需要将类型用逗号隔开即可。
泛型类一般用于类中的属性类型不确定的情况下。在声明属性时,使用下面的语句:
private data_type1 property_name1;
private data_type2 property_name2;
该语句中的 data_type1 与类声明中的 data_type1 表示的是同一种数据类型。
java
复制代码
示例:
在实例化泛型类时,需要指明泛型类中的类型参数,并赋予泛型类属性相应类型的值。例如,下面的示例代码创建了一个表示学生的泛型类,该类中包括 3 个属性,分别是姓名、年龄和性别。
public class Stu<N, A, S> {
private N name; // 姓名
private A age; // 年龄
private S sex; // 性别
// 创建类的构造函数
public Stu(N name, A age, S sex) {
this.name = name;
this.age = age;
this.sex = sex;
}
// 下面是上面3个属性的setter/getter方法
public N getName() {
return name;
}
public void setName(N name) {
this.name = name;
}
public A getAge() {
return age;
}
public void setAge(A age) {
this.age = age;
}
public S getSex() {
return sex;
}
public void setSex(S sex) {
this.sex = sex;
}
}
接着创建测试类。在测试类中调用 Stu 类的构造方法实例化 Stu 对象,并给该类中的 3 个属性赋予初始值,最终需要输出学生信息。测试类的代码实现如下:
public class Test14 {
public static void main(String[] args) {
Stu<String, Integer, Character> stu = new Stu<String, Integer, Character>("张晓玲", 28, '女');
String name = stu.getName();
Integer age = stu.getAge();
Character sex = stu.getSex();
System.out.println("学生信息如下:");
System.out.println("学生姓名:" + name + ",年龄:" + age + ",性别:" + sex);
}
}
6.7.3 泛型方法
java
复制代码
如果 static 方法需要使用泛型能力,就必须使其成为泛型方法。
定义泛型方法的语法格式如下:
[访问权限修饰符] [static] [final] <类型参数列表> 返回值类型 方法名([形式参数列表])
例如:
public static <T> List find(Class<T> cs,int userId){}
java
复制代码
示例:
一般来说编写 Java 泛型方法,其返回值类型至少有一个参数类型应该是泛型,而且类型应该是一致的,如果只有返回值类型或参数类型之一使用了泛型,那么这个泛型方法的使用就被限制了。下面就来定义一个泛型方法,具体介绍泛型方法的创建和使用。 使用泛型方法打印图书信息。定义泛型方法,参数类型使用"T"来代替。在方法的主体中打印出图书信息。代码的实现如下:
public class Test16 {
public static <T> void List(T book) { // 定义泛型方法
if (book != null) {
System.out.println(book);
}
}
public static void main(String[] args) {
Book stu = new Book(1, "细学 Java 编程", 28);
List(stu); // 调用泛型方法
}
}
6.7.4 泛型高级用法
java
复制代码
泛型的用法非常灵活,除在集合、类和方法中使用外,本节将从三个方面介绍泛型的高级用法,包括限制泛型可用类型、使用类型通配符、继承泛型类和实现泛型接口。
6.7.4.1 限制泛型可用类型
java
复制代码
1. 限制泛型可用类型
在 Java 中默认可以使用任何类型来实例化一个泛型类对象。当然也可以对泛型类实例的类型进行限制,anyClass 指某个接口或类。使用泛型限制后,泛型类的类型必须实现或继承 anyClass 这个接口或类。无论 anyClass 是接口还是类,在进行泛型限制时都必须使用 extends 关键字。语法格式如下:
class 类名称<T extends anyClass>
在下面的示例代码中创建了一个 ListClass 类,并对该类的类型限制为只能是实现 List 接口的类。
// 限制ListClass的泛型类型必须实现List接口
public class ListClass<T extends List> {
public static void main(String[] args) {
// 实例化使用ArrayList的泛型类ListClass,正确
ListClass<ArrayList> lc1 = new ListClass<ArrayList>();
// 实例化使用LinkedList的泛型类LlstClass,正确
ListClass<LinkedList> lc2 = new ListClass<LinkedList>();
// 实例化使用HashMap的泛型类ListClass,错误,因为HasMap没有实现List接口
// ListClass<HashMap> lc3=new ListClass<HashMap>();
}
}
6.7.4.2 使用类型通配符
java
复制代码
Java 中的泛型还支持使用类型通配符,它的作用是在创建一个泛型类对象时限制这个泛型类的类型必须实现或继承某个接口或类。
"<? extends List>"作为一个整体表示类型未知,当需要使用泛型对象时,可以单独实例化。使用泛型类型通配符的语法格式如下:
泛型类名称<? extends List>a = null;
示例代码演示了类型通配符的使用。
A<? extends List>a = null;
a = new A<ArrayList> (); // 正确
b = new A<LinkedList> (); // 正确
c = new A<HashMap> (); // 错误
6.7.4.3 继承泛型类和实现泛型接口
java
复制代码
定义为泛型的类和接口也可以被继承和实现。例如下面的示例代码演示了如何继承泛型类。
public class FatherClass<T1>{}
public class SonClass<T1,T2,T3> extents FatherClass<T1>{}
如果要在 SonClass 类继承 FatherClass 类时保留父类的泛型类型,需要在继承时指定,否则直接使用 extends FatherClass 语句进行继承操作,此时 T1、T2 和 T3 都会自动变为 Object,所以一般情况下都将父类的泛型类型保留。
下面的示例代码演示了如何在泛型中实现接口。
interface interface1<T1>{}
interface SubClass<T1,T2,T3> implements
Interface1<T2>{}
6.8 JAVA示例-图书信息查询
java
复制代码
使用 Map 映射来存储类别和图书信息,其键为 Category(类别)类型,值为 List<Book> 类型(Book 类为图书类),然后使用嵌套循环遍历输出每个类别所对应的多个图书信息。使用了泛型 List 和泛型 Map 分别存储图书类别和特定类别下的图书明细信息。
1)创建表示图书类别的 Category 类,在该类中有两个属性:id 和 name,分别表示编号和类别名称,并实现了它们的 setXxx() 和 getXxx() 方法,具体内容如下:
public class Category {
private int id; // 类别编号
private String name; // 类别名称
public Category(int id, String name) {
this.id = id;
this.name = name;
}
public String toString() {
return "所属分类:" + this.name;
}
// 上面两个属性的setXxx()和getXxx()方法
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
2)创建表示图书明细信息的 BookInfo 类,在该类中包含 5 个属性:id、name、price、author 和 startTime,分别表示图书编号、名称、价格、作者和出版时间,同样实现了它们的 setXxx() 和 getXxx() 方法,具体内容如下:
public class BookInfo {
private int id; // 编号
private String name; // 名称
private int price; // 价格
private String author; // 作者
private String startTime; // 出版时间
public BookInfo(int id, String name, int price, String author, String startTime) {
this.id = id;
this.name = name;
this.price = price;
this.author = author;
this.startTime = startTime;
}
public String toString() {
return this.id + "\t\t" + this.name + "\t\t" + this.price + "\t\t" + this.author + "\t\t" + this.startTime;
}
// 上面5个属性的 setXxx() 和 getXxx() 方法
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getPrice() {
return price;
}
public void setPrice(int price) {
this.id = price;
}
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
this.author = author;
}
public String getStartTime() {
return startTime;
}
public void setStartTime(String startTime) {
this.startTime = startTime;
}
}
3)创建 CategoryDao 类,在该类中定义一个泛型的 Map 映射,其键为 Category 类型的对象,值为 List<BookInfo> 类型的对象,并定义 printCategoryInfo() 方法,用于打印类别和图书明细信息。具体代码如下:
public class CategoryDao {
// 定义泛型Map,存储图书信息
public static Map<Category, List<BookInfo>> categoryMap = new HashMap<Category, List<BookInfo>>();
public static void printDeptmentInfo() {
for (Category cate : categoryMap.keySet()) {
System.out.println("所属类别:" + cate.getName());
List<BookInfo> books = categoryMap.get(cate);
System.out.println("图书编号\t\t图书名称\t\t图书价格\t\t图书作者\t\t出版时间");
for (int i = 0; i < books.size(); i++) {
BookInfo b = books.get(i); // 获取图书
System.out.println(b.getId() + "\t\t" + b.getName() + "\t\t" + b.getPrice() + "\t\t" + b.getAuthor()
+ "\t\t" + b.getStartTime());
}
System.out.println();
}
}
}
4)创建测试类 Test17,在该类中定义 4 个 Deptment 对象和 8 个 People 对象,并将 8 个 People 对象分成 4 组,存储到 4 个 List 集合中,然后将 4 个 Deptment 对象和 4 个 List 集合按照------对应的关系存储到 DeptmentDao 类中的 peoplesMap 映射中。最后调用 DeptmentDao 类中的 printDeptmentInfo() 方法打印类别及对应的图书信息。具体的代码如下:
public class Test17 {
public static void main(String[] args) {
Category category1 = new Category(1, "数据库"); // 创建类别信息
Category category2 = new Category(2, "程序设计"); // 创建类别信息
Category category3 = new Category(3, "平面设计"); // 创建类别信息
BookInfo book1 = new BookInfo(1, "细说 Java 编程", 25, "张晓玲", "2012-01-01"); // 创建图书信息
BookInfo book2 = new BookInfo(2, "影视后期处理宝典", 78, "刘水波", "2012-10-05"); // 创建图书信息
BookInfo book3 = new BookInfo(3, "MySQL 从入门到精通", 41, "王志亮", "2012-3-2"); // 创建图书信息
BookInfo book4 = new BookInfo(4, "Java 从入门到精通", 27, "陈奚静", "2012-11-01"); // 创建图书信息
BookInfo book5 = new BookInfo(5, "SQL Server 一百例", 68, "张晓玲", "2012-01-01"); // 创建图书信息
List<BookInfo> pList1 = new ArrayList<BookInfo>(); // 向类别 1 添加图书
pList1.add(book1);
pList1.add(book4);
List<BookInfo> pList2 = new ArrayList<BookInfo>(); // 向类别 2 添加图书
pList2.add(book3);
pList2.add(book5);
List<BookInfo> pList3 = new ArrayList<BookInfo>(); // 向类别 3 添加图书
pList3.add(book2);
CategoryDao.categoryMap.put(category1, pList1);
CategoryDao.categoryMap.put(category2, pList2);
CategoryDao.categoryMap.put(category3, pList3);
CategoryDao.printDeptmentInfo();
}
}
6.9 枚举
java
复制代码
枚举是一个被命名的整型常数的集合,用于声明一组带标识符的常数。枚举在曰常生活中很常见,例如一个人的性别只能是"男"或者"女",一周的星期只能是 7 天中的一个等。类似这种当一个变量有几种固定可能的取值时,就可以将它定义为枚举类型。
6.9.1 声明枚举
java
复制代码
声明枚举时必须使用 enum 关键字,然后定义枚举的名称、可访问性、基础类型和成员等。enum-modifiers 表示枚举的修饰符主要包括 public、private 和 internal;enumname 表示声明的枚举名称;enum-base 表示基础类型;enum-body 表示枚举的成员,它是枚举类型的命名常数。枚举声明的语法如下:
enum-modifiers enum enumname:enum-base {
enum-body,
}
java
复制代码
示例:
下面代码定义了一个表示性别的枚举类型 SexEnum 和一个表示颜色的枚举类型 Color。
public enum SexEnum {
male,female;
}
public enum Color {
RED,BLUE,GREEN,BLACK;
}
之后便可以通过枚举类型名直接引用常量,如 SexEnum.male、Color.RED。
使用枚举还可以使 switch 语句的可读性更强,例如以下示例代码:
enum Signal {
// 定义一个枚举类型
GREEN,YELLOW,RED
}
public class TrafficLight {
Signal color = Signal.RED;
public void change() {
switch(color) {
case RED:
color = Signal.GREEN;
break;
case YELLOW:
color = Signal.RED;
break;
case GREEN:
color = Signal.YELLOW;
break;
}
}
}
6.9.2 枚举类
java
复制代码
Java 中的每一个枚举都继承自 java.lang.Enum 类。当定义一个枚举类型时,每一个枚举类型成员都可以看作是 Enum 类的实例,这些枚举成员默认都被 final、public, static 修饰,当使用枚举类型成员时,直接使用枚举名称调用成员即可。所有枚举实例都可以调用 Enum 类的方法.
方法名称 |
描述 |
values() |
以数组形式返回枚举类型的所有成员 |
valueOf() |
将普通字符串转换为枚举实例 |
compareTo() |
比较两个枚举成员在定义时的顺序 |
ordinal() |
获取枚举成员的索引位置 |
6.9.2.1 values()方法
java
复制代码
示例:
通过调用枚举类型实例的 values( ) 方法可以将枚举的所有成员以数组形式返回,也可以通过该方法获取枚举类型的成员。
下面的示例创建一个包含 3 个成员的枚举类型 Signal,然后调用 values() 方法输出这些成员。
enum Signal {
// 定义一个枚举类型
GREEN,YELLOW,RED;
}
public static void main(String[] args) {
for(int i = 0;i < Signal.values().length;i++) {
System.out.println("枚举成员:"+Signal.values()[i]);
}
}
6.9.2.2 valueOf()方法
java
复制代码
创建一个示例,调用 valueOf() 方法获取枚举的一个成员,再调用 compareTo() 方法进行比较,并输出结果。具体实现代码如下:
public class TestEnum {
public enum Sex {
// 定义一个枚举
male,female;
}
public static void main(String[] args) {
compare(Sex.valueOf("male")); // 比较
}
public static void compare(Sex s) {
for(int i = 0;i < Sex.values().length;i++) {
System.out.println(s + "与" + Sex.values()[i] + "的比较结果是:" + s.compareTo(Sex.values()[i]));
}
}
}
6.9.2.3 ordinal() 方法
java
复制代码
通过调用枚举类型实例的ordinal() 方法可以获取一个成员在枚举中的索引位置。下面的示例创建一个包含 3 个成员的枚举类型 Signal,然后调用 ordinal() 方法输出成员及对应索引位置。
具体实现代码如下:
public class TestEnum1 {
enum Signal {
// 定义一个枚举类型
GREEN,YELLOW,RED;
}
public static void main(String[] args) {
for(int i = 0;i < Signal.values().length;i++) {
System.out.println("索引" + Signal.values()[i].ordinal()+",值:" + Signal.values()[i]);
}
}
}
6.9.3 为枚举添加方法
java
复制代码
Java 为枚举类型提供了一些内置的方法,同时枚举常量也可以有自己的方法。此时要注意必须在枚举实例的最后一个成员后添加分号,而且必须先定义枚举实例。
代码创建了一个枚举类型 WeekDay,而且在该类型中添加了自定义的方法。
enum WeekDay {
Mon("Monday"),Tue("Tuesday"),Wed("Wednesday"),Thu("Thursday"),Fri("Friday"),Sat("Saturday"),Sun("Sunday");
// 以上是枚举的成员,必须先定义,而且使用分号结束
private final String day;
private WeekDay(String day) {
this.day = day;
}
public static void printDay(int i) {
switch(i) {
case 1:
System.out.println(WeekDay.Mon);
break;
case 2:
System.out.println(WeekDay.Tue);
break;
case 3:
System.out.println(WeekDay.Wed);
break;
case 4:
System.out.println(WeekDay.Thu);
break;
case 5:
System.out.println(WeekDay.Fri);
break;
case 6:
System.out.println(WeekDay.Sat);
break;
case 7:
System.out.println(WeekDay.Sun);
break;
default:
System.out.println("wrong number!");
}
}
public String getDay() {
return day;
}
}
上面代码创建了 WeekDay 枚举类型,下面遍历该枚举中的所有成员,并调用 printDay() 方法。示例代码如下:
public static void main(String[] args) {
for(WeekDay day : WeekDay.values()) {
System.out.println(day+"====>" + day.getDay());
}
WeekDay.printDay(5);
}
Java 中的 enum 还可以跟 Class 类一样覆盖基类的方法。下面示例代码创建的 Color 枚举类型覆盖了 toString() 方法。
public class Test {
public enum Color {
RED("红色",1),GREEN("绿色",2),WHITE("白色",3),YELLOW("黄色",4);
// 成员变量
private String name;
private int index;
// 构造方法
private Color(String name,int index) {
this.name = name;
this.index = index;
}
// 覆盖方法
@Override
public String toString() {
return this.index + "-" + this.name;
}
}
public static void main(String[] args) {
System.out.println(Color.RED.toString()); // 输出:1-红色
}
}
6.9.3.1 EnumMap与EnumSet
java
复制代码
为了更好地支持枚举类型,java.util 中添加了两个新类:EnumMap 和 EnumSet。使用它们可以更高效地操作枚举类型。
6.9.3.2 EnumMap 类
java
复制代码
EnumMap 是专门为枚举类型量身定做的 Map 实现。虽然使用其他的 Map(如 HashMap)实现也能完成枚举类型实例到值的映射,但是使用 EnumMap 会更加高效。
HashMap 只能接收同一枚举类型的实例作为键值,并且由于枚举类型实例的数量相对固定并且有限,所以 EnumMap 使用数组来存放与枚举类型对应的值,使得 EnumMap 的效率非常高。
下面是使用 EnumMap 的一个代码示例。枚举类型 DataBaseType 里存放了现在支持的所有数据库类型。针对不同的数据库,一些数据库相关的方法需要返回不一样的值,例如示例中 getURL() 方法。
// 定义数据库类型枚举
public enum DataBaseType {
MYSQUORACLE,DB2,SQLSERVER
}
// 某类中定义的获取数据库URL的方法以及EnumMap的声明
private EnumMap<DataBaseType,String>urls = new EnumMap<DataBaseType,String>(DataBaseType.class);
public DataBaseInfo() {
urls.put(DataBaseType.DB2,"jdbc:db2://localhost:5000/sample");
urls.put(DataBaseType.MYSQL,"jdbc:mysql://localhost/mydb");
urls.put(DataBaseType.ORACLE,"jdbc:oracle:thin:@localhost:1521:sample");
urls.put(DataBaseType.SQLSERVER,"jdbc:microsoft:sqlserver://sql:1433;Database=mydb");
}
//根据不同的数据库类型,返回对应的URL
// @param type DataBaseType 枚举类新实例
// @return
public String getURL(DataBaseType type) {
return this.urls.get(type);
}
6.9.3.3 EnumSet类
java
复制代码
EnumSet 是枚举类型的高性能 Set 实现,它要求放入它的枚举常量必须属于同一枚举类型。
方法名称 |
描述 |
allOf(Class element type) |
创建一个包含指定枚举类型中所有枚举成员的 EnumSet 对象 |
complementOf(EnumSet s) |
创建一个与指定 EnumSet 对象 s 相同的枚举类型 EnumSet 对象, 并包含所有 s 中未包含的枚举成员 |
copyOf(EnumSet s) |
创建一个与指定 EnumSet 对象 s 相同的枚举类型 EnumSet 对象, 并与 s 包含相同的枚举成员 |
noneOf(<Class elementType) |
创建指定枚举类型的空 EnumSet 对象 |
of(E first,e...rest) |
创建包含指定枚举成员的 EnumSet 对象 |
range(E from ,E to) |
创建一个 EnumSet 对象,该对象包含了 from 到 to 之间的所有枚 举成员 |
java
复制代码
示例:
numSet 作为 Set 接口实现,它支持对包含的枚举常量的遍历。
for(Operation op:EnumSet.range(Operation.PLUS,Operation.MULTIPLY)) {
doSomeThing(op);
}
6.10 JAVA示例
6.10.1 一对多关系示例
java
复制代码
如一个学校可以包含多个学生,一个学生属于一个学校,那么这就是一个典型的一对多关系,可以通过集合进行关系的表示。下面是基于集合应用的一个示例,这个示例将作为以后 Java EE 开发的基础。
1)定义学生类
import java.util.HashSet;
import java.util.Iterator;
public class Student {
private String name; // 定义student类
private int age; // 定义name属性
private School school; // 一个学生属于一个学校
// 通过构造方法设置内容
public Student(String name, int age) {
this.setName(name);
this.setAge(age);
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public School getSchool() {
return school;
}
public void setSchool(School school) {
this.school = school;
}
// 重写toString()方法
public String toString() {
return "学生姓名:" + this.name + ":年龄" + this.age;
}
}
在以上的 Student 类中包含了一个 School 属性,表示一个学生属于一个学校。在程序运行时,只需要传入 School 类的引用就可以完成这样的关系。
2)定义学校类
import java.util.ArrayList;
import java.util.List;
public class School {
private String name;
private List<Student> allStudents; // 一个学校有多个学生
public School() {
this.allStudents = new ArrayList<Student>();// 实例化List集合
}
public School(String name) {
this();
this.setName(name);
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public List<Student> getAllStudents() {
return allStudents;
}
public void setAllStudents(List<Student> allStudents) {
this.allStudents = allStudents;
}
// 重写toString()方法
public String toString() {
return "学校名称:" + this.name;
}
}
在定义学校类时定义了一个 List 类型的属性,并指定其泛型类型是 Student 类型,这样一来就表示在一个 School 对象中会包含多个 Student 类型的引用。
3)测试代码,设置关系
import java.util.Iterator;
public class Test {
public static void main(String[] args) {
// 实例化学校对象
School sch = new School("清华大学");
// 实例化学生对象
Student s1 = new Student("张三", 21);
Student s2 = new Student("李四", 22);
Student s3 = new Student("王五", 23);
// 在学校中加入学生
sch.getAllStudents().add(s1);
sch.getAllStudents().add(s2);
sch.getAllStudents().add(s3);
// 一个学生属于一个学校
s1.setSchool(sch);
s2.setSchool(sch);
s3.setSchool(sch);
// 输出学校信息
System.out.println(sch);
// 实例化Iterator对象,用于输出全部的学生信息
Iterator<Student> ite = sch.getAllStudents().iterator();
while (ite.hasNext()) {
System.out.println("\t" + ite.next());
}
}
}
6.10.2 多对多关系示例
java
复制代码
使用集合不仅可以表示一对一的关系,也可以表示多对多的关系。例如,一个学生可以选多门课程,一门课程可以有多个学生参加,那么这就是一个典型的多对多关系。
要完成上面要求,首先应该定义两个类,分别是学生信息(Student)类、课程信息(Course)类。在学生类中存在一个集合,保存全部的课程。同样,在课程类中也要存在一个集合,保存全部的学生。
1)定义学生类
public class Student {
private String name;
private int age;
private List<Course> allCourses; // 定义集合保存全部课程
private Student() {
this.allCourses = new ArrayList<Course>();// 实例化List集合
}
// 通过构造方法设置内容
public Student(String name, int age) {
// 调用无参构造
this();
this.setName(name);
this.setAge(age);
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public List<Course> getAllCourses() {
return allCourses;
}
public void setAllCourses(List<Course> allCourses) {
this.allCourses = allCourses;
}
// 重写toString()方法
public String toString() {
return "学生姓名:" + this.name + ":年龄" + this.age;
}
}
在学生类中存在一个 allCourses 的 List 集合,这样在程序运行时,一个学生类中可以保存多个 Course 对象。
2)定义课程类
public class Course {
private String name;
private int credit;
// 定义集合保存多个学生
private List<Student> allStudents;
private Course() {
// 实例化List集合
this.allStudents = new ArrayList<Student>();
}
public Course(String name, int credit) {
this();
this.setName(name);
this.setCredit(credit);
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getCredit() {
return credit;
}
public void setCredit(int credit) {
this.credit = credit;
}
public List<Student> getAllStudents() {
return allStudents;
}
public void setAllStudents(List<Student> allStudents) {
this.allStudents = allStudents;
}
// 重写toString()方法
public String toString() {
return "课程名称" + this.name + ";课程学分" + this.credit;
}
}
课程类与学生类一样,都定义了一个 List 集合,用于保存多个学生信息。
3)测试程序
public class TestMore {
public static void main(String[] args) {
// 实例化课程对象
Course c1 = new Course("英语", 3);
Course c2 = new Course("计算机", 5);
// 实例化学生对象
Student s1 = new Student("张三", 20);
Student s2 = new Student("李四", 21);
Student s3 = new Student("王五", 22);
Student s4 = new Student("赵六", 23);
Student s5 = new Student("孙七", 24);
Student s6 = new Student("钱八", 25);
// 第一门课程有3个人参加,向课程中增加3个学生信息,同时向学生中增加课程信息
c1.getAllStudents().add(s1);
c1.getAllStudents().add(s2);
c1.getAllStudents().add(s6);
s1.getAllCourses().add(c1);
s2.getAllCourses().add(c1);
s6.getAllCourses().add(c1);
// 第二门课程有6个人参加,向课程中增加6个学生信息,同时向学生中添加课程信息
// 向课程中增加学生信息
c2.getAllStudents().add(s1);
c2.getAllStudents().add(s2);
c2.getAllStudents().add(s3);
c2.getAllStudents().add(s4);
c2.getAllStudents().add(s5);
c2.getAllStudents().add(s6);
// 像学生中增加课程信息
s1.getAllCourses().add(c2);
s2.getAllCourses().add(c2);
s3.getAllCourses().add(c2);
s4.getAllCourses().add(c2);
s5.getAllCourses().add(c2);
s6.getAllCourses().add(c2);
// 输出一门课程的信息,观察一门课程有多少个学生参加
System.out.println(c1); // 输出第一门课程
Iterator<Student> iter1 = c1.getAllStudents().iterator();
// 迭代输出
while (iter1.hasNext()) {
Student s = iter1.next();
System.out.println("\t" + s);
}
// 输出一个学生参加的课程信息,观察有多少门课程
System.out.println(s6);
Iterator<Course> iter2 = s6.getAllCourses().iterator();
while (iter2.hasNext()) {
// 取得所参加的课程
Course c = iter2.next();
// 输出课程信息
System.out.println("\t" + c);
}
}
}
七、反射机制
java
复制代码
Java 反射机制是 Java 语言的一个重要特性。学习 Java 反射机制前,应该先了解两个概念,编译期和运行期。
编译期:是指把源码交给编译器编译成计算机可以执行的文件的过程。在 Java 中也就是把 Java 代码编成class文件的过程。编译期只是做了一些翻译功能,并没有把代码放在内存中运行起来,而只是把代码当成文本进行操作,比如检查错误。
运行期:是把编译后的文件交给计算机执行,直到程序运行结束。所谓运行期就把在磁盘中的代码放到内存中执行起来。
Java 反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;这种动态获取信息以及动态调用对象方法的功能称为 Java 语言的反射机制。简单来说,反射机制指的是程序在运行时能够获取自身的信息。在 Java 中,只要给定类的名字,就可以通过反射机制来获得类的所有信息。
Java 反射机制在服务器程序和中间件程序中得到了广泛运用。在服务器端,往往需要根据客户的请求,动态调用某一个对象的特定方法。此外,在 ORM 中间件的实现中,运用 Java 反射机制可以读取任意一个 JavaBean 的所有属性,或者给这些属性赋值。
java
复制代码
Java 反射机制主要提供了以下功能,这些功能都位于java.lang.reflect包。
* 在运行时判断任意一个对象所属的类。
* 在运行时构造任意一个类的对象。
* 在运行时判断任意一个类所具有的成员变量和方法。
* 在运行时调用任意一个对象的方法。
* 生成动态代理。
所有 Java 类均继承了 Object 类,在 Object 类中定义了一个 getClass() 方法,该方法返回同一个类型为 Class 的对象。例如,下面的示例代码:
Class labelCls = label1.getClass(); // label1为 JLabel 类的对象
类型 |
访问方法 |
返回值类型 |
说明 |
包路径 |
getPackage() |
Package 对象 |
获取该类的存放路径 |
类名称 |
getName() |
String 对象 |
获取该类的名称 |
继承类 |
getSuperclass() |
Class 对象 |
获取该类继承的类 |
实现接口 |
getlnterfaces() |
Class 型数组 |
获取该类实现的所有接口 |
构造方法 |
getConstructors() |
Constructor 型数组 |
获取所有权限为 public 的构造方法 |
getDeclaredContruectors() |
Constructor 对象 |
获取当前对象的所有构造方法 |
|
方法 |
getMethods() |
Methods 型数组 |
获取所有权限为 public 的方法 |
getDeclaredMethods() |
Methods 对象 |
获取当前对象的所有方法 |
|
成员变量 |
getFields() |
Field 型数组 |
获取所有权限为 public 的成员变量 |
getDeclareFileds() |
Field 对象 |
获取当前对象的所有成员变量 |
|
内部类 |
getClasses() |
Class 型数组 |
获取所有权限为 public 的内部类 |
getDeclaredClasses() |
Class 型数组 |
获取所有内部类 |
|
内部类的声明类 |
getDeclaringClass() |
Class 对象 |
如果该类为内部类,则返回它的成员类,否则返回 null |
7.1 反射机制优缺点
java
复制代码
优点:
1. 能够运行时动态获取类的实例,大大提高系统的灵活性和扩展性。
2. 与 Java 动态编译相结合,可以实现无比强大的功能。
3. 对于 Java 这种先编译再运行的语言,能够让我们很方便的创建灵活的代码,这些代码可以在运行时装配,无需在组件之间进行源代码的链接,更加容易实现面向对象。
缺点:
1. 反射会消耗一定的系统资源,因此,如果不需要动态地创建一个对象,那么就不需要用反射;
2. 反射调用方法时可以忽略权限检查,获取这个类的私有方法和属性,因此可能会破坏类的封装性而导致安全问题。
7.2 反射机制API
java
复制代码
实现 Java 反射机制的类都位于 java.lang.reflect 包中,java.lang.Class 类是 Java 反射机制 API 中的核心类。
7.2.1 java.lang.Class类
java
复制代码
java.lang.Class 类是实现反射的关键所在,Class 类的一个实例表示 Java 的一种数据类型,包括类、接口、枚举、注(Annotation)、数组、基本数据类型和 void。Class 没有公有的构造方法,Class 实例是由 JVM 在类加载时自动创建的。
程序代码中获得 Class 实例可以通过如下代码实现:
// 1. 通过类型class静态变量
Class clz1 = String.class;
String str = "Hello";
// 2. 通过对象的getClass()方法
Class clz2 = str.getClass();
每一种类型包括类和接口等,都有一个 class 静态变量可以获得 Class 实例。另外,每一个对象都有 getClass() 方法可以获得 Class 实例,该方法是由 Object 类提供的实例方法。Class 类提供了很多方法可以获得运行时对象的相关信息:
public class ReflectionTest01 {
public static void main(String[] args) {
// 获得Class实例
// 1.通过类型class静态变量
Class clz1 = String.class;
String str = "Hello";
// 2.通过对象的getClass()方法
Class clz2 = str.getClass();
// 获得int类型Class实例
Class clz3 = int.class;
// 获得Integer类型Class实例
Class clz4 = Integer.class;
System.out.println("clz2类名称:" + clz2.getName());
System.out.println("clz2是否为接口:" + clz2.isInterface());
System.out.println("clz2是否为数组对象:" + clz2.isArray());
System.out.println("clz2父类名称:" + clz2.getSuperclass().getName());
System.out.println("clz2是否为基本类型:" + clz2.isPrimitive());
System.out.println("clz3是否为基本类型:" + clz3.isPrimitive());
System.out.println("clz4是否为基本类型:" + clz4.isPrimitive());
}
}
7.2.2 java.lang.reflect包
java
复制代码
java.lang.reflect 包提供了反射中用到类,主要的类说明如下:
* Constructor 类:提供类的构造方法信息。
* Field 类:提供类或接口中成员变量信息。
* Method 类:提供类或接口成员方法信息。
* Array 类:提供了动态创建和访问 Java 数组的方法。
* Modifier 类:提供类和成员访问修饰符信息。
示例:
过 Class 的静态方法forName(String)创建某个类的运行时对象,其中的参数是类全名字符串,如果在类路径中找不到这个类则抛出 ClassNotFoundException 异常,见代码第 17 行。
代码第 7 行是通过 Class 的实例方法 getDeclaredMethods() 返回某个类的成员方法对象数组。代码第 9 行是遍历成员方法集合,其中的元素是 Method 类型。
代码第 11 行的method.getModifiers()方法返回访问权限修饰符常量代码,是 int 类型,例如 1 代表 public,这些数字代表的含义可以通过Modifier.toString(int)方法转换为字符串。代码第 13 行通过 Method 的 getReturnType() 方法获得方法返回值类型,然后再调用 getName() 方法返回该类型的名称。代码第 15 行method.getName()返回方法名称。
public class ReflectionTest02 {
public static void main(String[] args) {
try {
// 动态加载xx类的运行时对象
Class c = Class.forName("java.lang.String");
// 获取成员方法集合
Method[] methods = c.getDeclaredMethods();
// 遍历成员方法集合
for (Method method : methods) {
// 打印权限修饰符,如public、protected、private
System.out.print(Modifier.toString(method.getModifiers()));
// 打印返回值类型名称
System.out.print(" " + method.getReturnType().getName() + " ");
// 打印方法名称
System.out.println(method.getName() + "();");
}
} catch (ClassNotFoundException e) {
System.out.println("找不到指定类");
}
}
}
7.3 通过反射访问构造方法
java
复制代码
为了能够动态获取对象构造方法的信息,首先需要通过下列方法之一创建一个 Constructor 类型的对象或者数组。
* getConstructors()
* getConstructor(Class<?>...parameterTypes)
* getDeclaredConstructors()
* getDeclaredConstructor(Class<?>...parameterTypes)
如果是访问指定的构造方法,需要根据该构造方法的入口参数的类型来访问。例如,访问一个入口参数类型依次为 int 和 String 类型的构造方法,下面的两种方式均可以实现。
1. objectClass.getDeclaredConstructor(int.class,String.class);
2. objectClass.getDeclaredConstructor(new Class[]{int.class,String.class});
下列代码判断对象 con 所代表的构造方法是否被 public 修饰,以及以字符串形式获取该构造方法的所有修饰符。
int modifiers = con.getModifiers(); // 获取构造方法的修饰符整数
boolean isPublic = Modifier.isPublic(modifiers); // 判断修饰符整数是否为public
string allModifiers = Modifier.toString(modifiers);
方法名称 |
说明 |
isVarArgs() |
查看该构造方法是否允许带可变数量的参数,如果允许,返回 true,否则返回 false |
getParameterTypes() |
按照声明顺序以 Class 数组的形式获取该构造方法各个参数的类型 |
getExceptionTypes() |
以 Class 数组的形式获取该构造方法可能抛出的异常类型 |
newInstance(Object ... initargs) |
通过该构造方法利用指定参数创建一个该类型的对象,如果未设置参数则表示 采用默认无参的构造方法 |
setAccessiable(boolean flag) |
如果该构造方法的权限为 private,默认为不允许通过反射利用 netlnstance() 方法创建对象。如果先执行该方法,并将入口参数设置为 true,则允许创建对 象 |
getModifiers() |
获得可以解析出该构造方法所采用修饰符的整数 |
静态方法名称 |
说明 |
isStatic(int mod) |
如果使用 static 修饰符修饰则返回 true,否则返回 false |
isPublic(int mod) |
如果使用 public 修饰符修饰则返回 true,否则返回 false |
isProtected(int mod) |
如果使用 protected 修饰符修饰则返回 true,否则返回 false |
isPrivate(int mod) |
如果使用 private 修饰符修饰则返回 true,否则返回 false |
isFinal(int mod) |
如果使用 final 修饰符修饰则返回 true,否则返回 false |
toString(int mod) |
以字符串形式返回所有修饰符 |
java
复制代码
示例:
1)首先创建一个 Book 类表示图书信息。在该类中声明一个 String 型变量表示图书名称,两个 int 型变量分别表示图书编号和价格,并提供 3 个构造方法。
Book 类的最终代码如下:
public class Book {
String name; // 图书名称
int id, price; // 图书编号和价格
// 空的构造方法
private Book() {
}
// 带两个参数的构造方法
protected Book(String _name, int _id) {
this.name = _name;
this.id = _id;
}
// 带可变参数的构造方法
public Book(String... strings) throws NumberFormatException {
if (0 < strings.length)
id = Integer.valueOf(strings[0]);
if (1 < strings.length)
price = Integer.valueOf(strings[1]);
}
// 输出图书信息
public void print() {
System.out.println("name=" + name);
System.out.println("id=" + id);
System.out.println("price=" + price);
}
}
2)编写测试类 Test01,在该类的 main() 方法中通过反射访问 Book 类中的所有构造方法,并将该构造方法是否带可变类型参数、入口参数类型和可能拋出的异常类型信息输出到控制台。
Test01 类的代码如下:
public class Test01 {
public static void main(String[] args) {
// 获取动态类Book
Class book = Book.class;
// 获取Book类的所有构造方法
Constructor[] declaredContructors = book.getDeclaredConstructors();
// 遍历所有构造方法
for (int i = 0; i < declaredContructors.length; i++) {
Constructor con = declaredContructors[i];
// 判断构造方法的参数是否可变
System.out.println("查看是否允许带可变数量的参数:" + con.isVarArgs());
System.out.println("该构造方法的入口参数类型依次为:");
// 获取所有参数类型
Class[] parameterTypes = con.getParameterTypes();
for (int j = 0; j < parameterTypes.length; j++) {
System.out.println(" " + parameterTypes[j]);
}
System.out.println("该构造方法可能拋出的异常类型为:");
// 获取所有可能拋出的异常类型
Class[] exceptionTypes = con.getExceptionTypes();
for (int j = 0; j < exceptionTypes.length; j++) {
System.out.println(" " + parameterTypes[j]);
}
// 创建一个未实例化的Book类实例
Book book1 = null;
while (book1 == null) {
try { // 如果该成员变量的访问权限为private,则拋出异常
if (i == 1) {
// 通过执行带两个参数的构造方法实例化book1
book1 = (Book) con.newInstance("Java 教程", 10);
} else if (i == 2) {
// 通过执行默认构造方法实例化book1
book1 = (Book) con.newInstance();
} else {
// 通过执行可变数量参数的构造方法实例化book1
Object[] parameters = new Object[] { new String[] { "100", "200" } };
book1 = (Book) con.newInstance(parameters);
}
} catch (Exception e) {
System.out.println("在创建对象时拋出异常,下面执行 setAccessible() 方法");
con.setAccessible(true); // 设置允许访问 private 成员
}
}
book1.print();
System.out.println("=============================\n");
}
}
}
7.4 通过反射访问方法
java
复制代码
要动态获取一个对象方法的信息,首先需要通过下列方法之一创建一个 Method 类型的对象或者数组。
* getMethods()
* getMethods(String name,Class<?> ...parameterTypes)
* getDeclaredMethods()
* getDeclaredMethods(String name,Class<?>...parameterTypes)
如果是访问指定的构造方法,需要根据该方法的入口参数的类型来访问。例如,访问一个名称为 max,入口参数类型依次为 int 和 String 类型的方法。
下面的两种方式均可以实现:
objectClass.getDeclaredConstructor("max",int.class,String.class);
objectClass.getDeclaredConstructor("max",new Class[]{int.class,String.class});
静态方法名称 |
说明 |
getName() |
获取该方法的名称 |
getParameterType() |
按照声明顺序以 Class 数组的形式返回该方法各个参数的类型 |
getReturnType() |
以 Class 对象的形式获得该方法的返回值类型 |
getExceptionTypes() |
以 Class 数组的形式获得该方法可能抛出的异常类型 |
invoke(Object obj,Object...args) |
利用 args 参数执行指定对象 obj 中的该方法,返回值为 Object 类型 |
isVarArgs() |
查看该方法是否允许带有可变数量的参数,如果允许返回 true,否则返回 false |
getModifiers() |
获得可以解析出该方法所采用修饰符的整数 |
java
复制代码
示例:
1)首先创建一个 Book1 类,并编写 4 个具有不同作用域的方法。Book1 类的最终代码如下:
public class Book1 {
// static 作用域方法
static void staticMethod() {
System.out.println("执行staticMethod()方法");
}
// public 作用域方法
public int publicMethod(int i) {
System.out.println("执行publicMethod()方法");
return 100 + i;
}
// protected 作用域方法
protected int protectedMethod(String s, int i) throws NumberFormatException {
System.out.println("执行protectedMethod()方法");
return Integer.valueOf(s) + i;
}
// private 作用域方法
private String privateMethod(String... strings) {
System.out.println("执行privateMethod()方法");
StringBuffer sb = new StringBuffer();
for (int i = 0; i < sb.length(); i++) {
sb.append(strings[i]);
}
return sb.toString();
}
}
2)编写测试类 Test02,在该类的 main() 方法中通过反射访问 Book1 类中的所有方法,并将该方法是否带可变类型参数、入口参数类型和可能拋出的异常类型信息输出到控制台。
Test02 类的代码如下:
public class Test02 {
public static void main(String[] args) {
// 获取动态类Book1
Book1 book = new Book1();
Class class1 = book.getClass();
// 获取Book1类的所有方法
Method[] declaredMethods = class1.getDeclaredMethods();
for (int i = 0; i < declaredMethods.length; i++) {
Method method = declaredMethods[i];
System.out.println("方法名称为:" + method.getName());
System.out.println("方法是否带有可变数量的参数:" + method.isVarArgs());
System.out.println("方法的参数类型依次为:");
// 获取所有参数类型
Class[] methodType = method.getParameterTypes();
for (int j = 0; j < methodType.length; j++) {
System.out.println(" " + methodType[j]);
}
// 获取返回值类型
System.out.println("方法的返回值类型为:" + method.getReturnType());
System.out.println("方法可能抛出的异常类型有:");
// 获取所有可能抛出的异常
Class[] methodExceptions = method.getExceptionTypes();
for (int j = 0; j < methodExceptions.length; j++) {
System.out.println(" " + methodExceptions[j]);
}
boolean isTurn = true;
while (isTurn) {
try { // 如果该成员变量的访问权限为private,则抛出异常
isTurn = false;
if (method.getName().equals("staticMethod")) { // 调用没有参数的方法
method.invoke(book);
} else if (method.getName().equals("publicMethod")) { // 调用一个参数的方法
System.out.println("publicMethod(10)的返回值为:" + method.invoke(book, 10));
} else if (method.getName().equals("protectedMethod")) { // 调用两个参数的方法
System.out.println("protectedMethod(\"10\",15)的返回值为:" + method.invoke(book, "10", 15));
} else if (method.getName().equals("privateMethod")) { // 调用可变数量参数的方法
Object[] parameters = new Object[] { new String[] { "J", "A", "V", "A" } };
System.out.println("privateMethod()的返回值为:" + method.invoke(book, parameters));
}
} catch (Exception e) {
System.out.println("在设置成员变量值时抛出异常,下面执行setAccessible()方法");
method.setAccessible(true); // 设置为允许访问private方法
isTurn = true;
}
}
System.out.println("=============================\n");
}
}
}
7.5 反射访问成员变量
java
复制代码
通过下列任意一个方法访问成员变量时将返回 Field 类型的对象或数组。
* getFields()
* getField(String name)
* getDeclaredFields()
* getDeclaredField(String name)
上述方法返回的 Field 对象代表一个成员变量。例如,要访问一个名称为 price 的成员变量,示例代码如下:
object.getDeciaredField("price");
方法名称 |
说明 |
getName() |
获得该成员变量的名称 |
getType() |
获取表示该成员变量的 Class 对象 |
get(Object obj) |
获得指定对象 obj 中成员变量的值,返回值为 Object 类型 |
set(Object obj, Object value) |
将指定对象 obj 中成员变量的值设置为 value |
getlnt(0bject obj) |
获得指定对象 obj 中成员类型为 int 的成员变量的值 |
setlnt(0bject obj, int i) |
将指定对象 obj 中成员变量的值设置为 i |
setFloat(Object obj, float f) |
将指定对象 obj 中成员变量的值设置为 f |
getBoolean(Object obj) |
获得指定对象 obj 中成员类型为 boolean 的成员变量的值 |
setBoolean(Object obj, boolean b) |
将指定对象 obj 中成员变量的值设置为 b |
getFloat(Object obj) |
获得指定对象 obj 中成员类型为 float 的成员变量的值 |
setAccessible(boolean flag) |
此方法可以设置是否忽略权限直接访问 private 等私有权限的成员变量 |
getModifiers() |
获得可以解析出该方法所采用修饰符的整数 |
java
复制代码
示例:
1)首先创建一个 Book2 类,在该类中依次声明一个 String、int、float 和 boolean 类型的成员,并设置不同的访问作用域。Book2 类最终的代码如下:
public class Book2 {
String name;
public int id;
private float price;
protected boolean isLoan;
}
2)编写测试类 Test03,在该类的 main() 方法中通过反射访问 Book2 类中的所有成员,并将该成员的名称和类型信息输出到控制台。
Test03 类的代码如下:
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
public class Test03 {
public static void main(String[] args) {
Book2 book = new Book2();
// 获取动态类Book2
Class class1 = book.getClass();
// 获取Book2类的所有成员
Field[] declaredFields = class1.getDeclaredFields();
// 遍历所有的成员
for(int i = 0;i < declaredFields.length;i++) {
// 获取类中的成员变量
Field field = declaredFields[i];
System.out.println("成员名称为:" + field.getName());
Class fieldType = field.getType();
System.out.println("成员类型为:" + fieldType);
boolean isTurn = true;
while(isTurn) {
try {
// 如果该成员变量的访问权限为private,则抛出异常
isTurn = false;
System.out.println("修改前成员的值为:" + field.get(book));
// 判断成员类型是否为int
if(fieldType.equals(int.class)) {
System.out.println("利用setInt()方法修改成员的值");
field.setInt(book, 100);
} else if(fieldType.equals(float.class)) {
// 判断成员变量类型是否为float
System.out.println("利用setFloat()方法修改成员的值");
field.setFloat(book, 29.815f);
} else if(fieldType.equals(boolean.class)) {
// 判断成员变量是否为boolean
System.out.println("利用setBoolean()方法修改成员的值");
field.setBoolean(book, true);
} else {
System.out.println("利用set()方法修改成员的值");
field.set(book, "Java编程");
}
System.out.println("修改后成员的值为:" + field.get(book));
} catch (Exception e) {
System.out.println("在设置成员变量值时抛出异常,下面执行setAccessible()方法");
field.setAccessible(true);
isTurn = true;
}
}
System.out.println("=============================\n");
}
}
}
7.6 远程方法中使用反射机制
java
复制代码
反射机制在网络编程中的应用,实现如何在客户端通过远程方法调用服务器端的方法。
java
复制代码
示例:
假定在服务器端有一个 HelloService 接口,该接口具有 getTime() 和 echo() 方法,具体代码如下:
import java.util.Date;
public interface HelloService {
public String echo(String msg);
public Date getTime();
}
在服务器上创建一个 HelloServiceImpl 类并实现 HelloService 接口。HelloServiceImpl 类的代码如下:
import java.util.Date;
public class HelloServiceImpl implements HelloService {
@Override
public String echo(String msg) {
return "echo:" + msg;
}
@Override
public Date getTime() {
return new Date();
}
}
上述代码所示,在 HelloServiceImpl 类中对 echo() 方法和 getTime() 方法进行了重写。那么,客户端如何调用服务器端 Hello-ServiceImpl 类中的 getTime() 和 echo() 方法呢?
具体方法是:客户端需要把调用的方法名、方法参数类型、方法参数值,以及方法所属的类名或接口名发送给服务器端。服务器端再调用相关对象的方法,然后把方法的返回值发送给客户端。
为了便于按照面向对象的方式来处理客户端与服务器端的通信,可以把它们发送的信息用 Call 类来表示。一个 Call 对象表示客户端发起的一个远程调用,它包括调用的类名或接口名、方法名、方法参数类型、方法参数值和方法执行结果。
Call 类的实现代码如下:
import java.io.Serializable;
public class Call implements Serializable {
private static final long serialVersionUID = 6659953547331194808L;
private String className; // 表示类名或接口名
private String methodName; // 表示方法名
private Class[] paramTypes; // 表示方法参数类型
private Object[] params; // 表示方法参数值
// 表示方法的执行结果
// 如果方法正常执行,则result为方法返回值,如果方法抛出异常,那么result为该异常。
private Object result;
public Call() {
}
public Call(String className, String methodName, Class[] paramTypes, Object[] params) {
this.className = className;
this.methodName = methodName;
this.paramTypes = paramTypes;
this.params = params;
}
public String getClassName() {
return className;
}
public void setClassName(String className) {
this.className = className;
}
public String getMethodName() {
return methodName;
}
public void setMethodName(String methodName) {
this.methodName = methodName;
}
public Class[] getParamTypes() {
return paramTypes;
}
public void setParamTypes(Class[] paramTypes) {
this.paramTypes = paramTypes;
}
public Object[] getParams() {
return params;
}
public void setParams(Object[] params) {
this.params = params;
}
public Object getResult() {
return result;
}
public void setResult(Object result) {
this.result = result;
}
public String toString() {
return "className=" + className + "methodName=" + methodName;
}
}
假设客户端为 SimpleClient,服务器端为 SimpleServer。SimpleClient 调用 SimpleServer 的 HelloServiceImpl 对象中 echo() 方法的流程如下:
* SimpleClient 创建一个 Call 对象,它包含调用 HelloService 接口的 echo() 方法的信息。
* SimpleClient 通过对象输出流把 Call 对象发送给 SimpleServer。
* SimpleServer 通过对象输入流读取 Call 对象,运用反射机制调用 HelloServiceImpl 对象的 echo() 方法,把 echo() 方法的执行结果保存到 Call 对象中。
* SimpleServer 通过对象输出流把包含方法执行结果的 Call 对象发送给 SimpleClient。
* SimpleClient 通过对象输入流读取 Call 对象,从中获得方法执行结果。
首先来看看客户端程序 SimpleClient 类的实现代码。
import java.io.*;
import java.net.*;
import java.util.*;
import java.lang.reflect.*;
import java.io.*;
import java.net.*;
import java.util.*;
public class SimpleClient {
public void invoke() throws Exception {
Socket socket = new Socket("localhost", 8000);
OutputStream out = socket.getOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(out);
InputStream in = socket.getInputStream();
ObjectInputStream ois = new ObjectInputStream(in);
// 创建一个远程调用对象
Call call = new Call("ch12.HelloService", "echo", new Class[] { String.class }, new Object[] { "Java" });
oos.writeObject(call); // 向服务器发送Call对象
call = (Call) ois.readObject(); // 接收包含了方法执行结果的Call对象
System.out.println(call.getResult());
ois.close();
oos.close();
socket.close();
}
public static void main(String args[]) throws Exception {
new SimpleClient().invoke();
}
}
如上述代码所示,客户端 SimpleClient 类的主要作用是建立与服务器的连接,然后将带有调用信息的 Call 对象发送到服务器端。
服务器端 SimpleServer 类在收到调用请求之后会使用反射机制动态调用指定对象的指定方法,再将执行结果返回给客户端。
SimpleServer 类的实现代码如下:
import java.io.*;
import java.net.*;
import java.util.*;
import java.lang.reflect.*;
public class SimpleServer {
private Map remoteObjects = new HashMap(); // 存放远程对象的缓存
/** 把一个远程对象放到缓存中 */
public void register(String className, Object remoteObject) {
remoteObjects.put(className, remoteObject);
}
public void service() throws Exception {
ServerSocket serverSocket = new ServerSocket(8000);
System.out.println("服务器启动.");
while (true) {
Socket socket = serverSocket.accept();
InputStream in = socket.getInputStream();
ObjectInputStream ois = new ObjectInputStream(in);
OutputStream out = socket.getOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(out);
Call call = (Call) ois.readObject(); // 接收客户发送的Call对象
System.out.println(call);
call = invoke(call); // 调用相关对象的方法
oos.writeObject(call); // 向客户发送包含了执行结果的Call对象
ois.close();
oos.close();
socket.close();
}
}
public Call invoke(Call call) {
Object result = null;
try {
String className = call.getClassName();
String methodName = call.getMethodName();
Object[] params = call.getParams();
Class classType = Class.forName(className);
Class[] paramTypes = call.getParamTypes();
Method method = classType.getMethod(methodName, paramTypes);
Object remoteObject = remoteObjects.get(className); // 从缓存中取出相关的远程对象
if (remoteObject == null) {
throw new Exception(className + "的远程对象不存在");
} else {
result = method.invoke(remoteObject, params);
}
} catch (Exception e) {
result = e;
}
call.setResult(result); // 设置方法执行结果
return call;
}
public static void main(String args[]) throws Exception {
SimpleServer server = new SimpleServer();
// 把事先创建的HelloServiceImpl对象加入到服务器的缓存中
server.register("ch13.HelloService", new HelloServiceImpl());
server.service();
}
}
由于这是一个网络程序,首先需要运行服务器端 SimpleServer,然后再运行客户端 SimpleClient。运行结果是在客户端看到输出"echoJava",这个结果是服务器端执行 HelloServicelmpl 对象的 echo() 方法的返回值。图 1 所示显示了 SimpleClient 与 SimpleServer 的通信过程。
八、输入与输出流
java
复制代码
根据数据流向的不同,可以分为输入(Input)流和输出(Output)流两种。
8.1 输入与输出流概念
java
复制代码
Java 程序通过流来完成输入/输出,所有的输入/输出以流的形式处理。因此要了解 I/O 系统,首先要理解输入/输出流的概念。
输入就是将数据从各种输入设备(包括文件、键盘等)中读取到内存中,输出则正好相反,是将数据写入到各种输出设备(比如文件、显示器、磁盘等)。例如键盘就是一个标准的输入设备,而显示器就是一个标准的输出设备,但是文件既可以作为输入设备,又可以作为输出设备。
数据流是 Java 进行 I/O 操作的对象,它按照不同的标准可以分为不同的类别。
* 按照流的方向主要分为输入流和输出流两大类。
* 数据流按照数据单位的不同分为字节流和字符流。
* 按照功能可以划分为节点流和处理流。
8.1.1 输入流
java
复制代码
Java 流相关的类都封装在 java.io 包中,而且每个数据流都是一个对象。所有输入流类都是 InputStream 抽象类(字节输入流)和 Reader 抽象类(字符输入流)的子类。
名称 |
作用 |
int read() |
从输入流读入一个 8 字节的数据,将它转换成一个 0~ 255 的整数,返回一个整数,如果遇到输入流的结尾返回 -1 |
int read(byte[] b) |
从输入流读取若干字节的数据保存到参数 b 指定的字节数组中,返回的字节数表示读取的字节数,如果遇到输入流的结尾返回 -1 |
int read(byte[] b,int off,int len) |
从输入流读取若干字节的数据保存到参数 b 指定的字节数组中,其中 off 是指在数组中开始保存数据位置的起始下标,len 是指读取字节的位数。返回的是实际读取的字节数,如果遇到输入流的结尾则返回 -1 |
void close() |
关闭数据流,当完成对数据流的操作之后需要关闭数据流 |
int available() |
返回可以从数据源读取的数据流的位数。 |
skip(long n) |
从输入流跳过参数 n 指定的字节数目 |
boolean markSupported() |
判断输入流是否可以重复读取,如果可以就返回 true |
void mark(int readLimit) |
如果输入流可以被重复读取,从流的当前位置开始设置标记,readLimit 指定可以设置标记的字节数 |
void reset() |
使输入流重新定位到刚才被标记的位置,这样可以重新读取标记过的数据 |
8.1.2 输出流
java
复制代码
在 Java 中所有输出流类都是 OutputStream 抽象类(字节输出流)和 Writer 抽象类(字符输出流)的子类。
名称 |
作用 |
int write(b) |
将指定字节的数据写入到输出流 |
int write (byte[] b) |
将指定字节数组的内容写入输出流 |
int write (byte[] b,int off,int len) |
将指定字节数组从 off 位置开始的 len 字节的内容写入输出流 |
close() |
关闭数据流,当完成对数据流的操作之后需要关闭数据流 |
flush() |
刷新输出流,强行将缓冲区的内容写入输出流 |
8.2 系统流
java
复制代码
每个 Java 程序运行时都带有一个系统流,系统流对应的类为 java.lang.System。Sytem 类封装了 Java 程序运行时的 3 个系统流,分别通过 in、out 和 err 变量来引用。这 3 个系统流如下所示:
* System.in:标准输入流,默认设备是键盘。
* System.out:标准输出流,默认设备是控制台。
* System.err:标准错误流,默认设备是控制台。
System.in 是 InputStream 类的一个对象,System.out 输出流主要用于将指定内容输出到控制台。System.out 和 System.error 是 PrintStream 类的对象。因为 PrintStream 是一个从 OutputStream 派生的输出流,所以它还执行低级别的 write() 方法。因此,除了 print() 和 println() 方法可以完成控制台输出以外,System.out 还可以调用 write() 方法实现控制台输出。
write() 方法的简单形式如下:
void write(int byteval) throws IOException
java
复制代码
示例:
使用 System.in 读取字节数组,使用 System.out 输出字节数组。
public class Test01 {
public static void main(String[] args) {
byte[] byteData = new byte[100]; // 声明一个字节数组
System.out.println("请输入英文:");
try {
System.in.read(byteData);
} catch (IOException e) {
e.printStackTrace();
}
System.out.println("您输入的内容如下:");
for (int i = 0; i < byteData.length; i++) {
System.out.print((char) byteData[i]);
}
}
}
8.3 字符编码
java
复制代码
Java 程序的开发中最常见的是 ISO8859-1、GBK/GB2312、Unicode、 UTF 编码。
Java 中常见编码说明如下:
* ISO8859-1:属于单字节编码,最多只能表示 0~255 的字符范围。
* GBK/GB2312:中文的国标编码,用来表示汉字,属于双字节编码。GBK 可以表示简体中文和繁体中文,而 GB2312 只能表示简体中文。GBK 兼容 GB2312。
* Unicode:是一种编码规范,是为解决全球字符通用编码而设计的。UTF-8 和 UTF-16 是这种规范的一种实现,此编码不兼容 ISO8859-1 编码。Java 内部采用此编码。
* UTF:UTF 编码兼容了 ISO8859-1 编码,同时也可以用来表示所有的语言字符,不过 UTF 编码是不定长编码,每一个字符的长度为 1~6 个字节不等。一般在中文网页中使用此编码,可以节省空间
Java 中 System 类可以取得与系统有关的信息,所以直接使用此类可以找到系统的默认编码。方法如下所示:
public static Properties getProperty()
示例1:
查看 JVM 的默认编码,代码如下:
public static void main(String[] args) {
// 获取当前系统编码
System.out.println("系统默认编码:" + System.getProperty("file.encoding"));
}
java
复制代码
示例2:
下面通过一个示例讲解乱码的产生。现在本地的默认编码是 GBK,下面通过 ISO8859-1 编码对文字进行编码转换。如果要实现编码的转换可以使用 String 类中的 getBytes(String charset) 方法,此方法可以设置指定的编码,该方法的格式如下:
public byte[] getBytes(String charset);
public class Test {
public static void main(String[] args) throws Exception {
File f = new File("D:" + File.separator + "test.txt");
// 实例化输出流
OutputStream out = new FileOutputStream(f);
// 指定ISO8859-1编码
byte b[] = "C语言中文网,你好!".getBytes("ISO8859-1");
// 保存转码之后的数据
out.write(b);
// 关闭输出流
out.close();
}
}
8.4 File类
java
复制代码
File 类是 java.io 包中唯一代表磁盘文件本身的对象,也就是说,如果希望在程序中操作文件和目录,则都可以通过 File 类来完成。File 类定义了一些方法来操作文件,如新建、删除、重命名文件和目录等。
* File 类不能访问文件内容本身,如果需要访问文件内容本身,则需要使用输入/输出流。
File类提供了如下三种形式构造方法。
1. File(String path):如果 path 是实际存在的路径,则该 File 对象表示的是目录;如果 path 是文件名,则该 File 对象表示的是文件。
2. File(String path, String name):path 是路径名,name 是文件名。
3. File(File dir, String name):dir 是路径对象,name 是文件名。
File 类中有以下两个常用常量:
1. public static final String pathSeparator:指的是分隔连续多个路径字符串的分隔符,Windows 下指;。例如 java -cp test.jar;abc.jar HelloWorld。
2. public static final String separator:用来分隔同一个路径字符串中的目录的,Windows 下指/。例如 C:/Program Files/Common Files
boolean canRead() |
测试应用程序是否能从指定的文件中进行读取 |
boolean canWrite() |
测试应用程序是否能写当前文件 |
boolean delete() |
删除当前对象指定的文件 |
boolean exists() |
测试当前 File 是否存在 |
String getAbsolutePath() |
返回由该对象表示的文件的绝对路径名 |
String getName() |
返回表示当前对象的文件名或路径名(如果是路径,则返回最后一级子路径名) |
String getParent() |
返回当前 File 对象所对应目录(最后一级子目录)的父目录名 |
boolean isAbsolute() |
测试当前 File 对象表示的文件是否为一个绝对路径名。该方法消除了不同平台的差异,可以直接判断 file 对象是否为绝对路径。在 UNIX/Linux/BSD 等系统上,如果路径名开头是一条斜线/ ,则表明该 File 对象对应一个绝对路径;在 Windows 等系统上,如果路径开头是盘符,则说明它是一个绝对路径。 |
boolean isDirectory() |
测试当前 File 对象表示的文件是否为一个路径 |
boolean isFile() |
测试当前 File 对象表示的文件是否为一个"普通"文件 |
long lastModified() |
返回当前 File 对象表示的文件最后修改的时间 |
long length() |
返回当前 File 对象表示的文件长度 |
String[] list() |
返回当前 File 对象指定的路径文件列表 |
String[] list(FilenameFilter) |
返回当前 File 对象指定的目录中满足指定过滤器的文件列表 |
boolean mkdir() |
创建一个目录,它的路径名由当前 File 对象指定 |
boolean mkdirs() |
创建一个目录,它的路径名由当前 File 对象指定 |
boolean renameTo(File) |
将当前 File 对象指定的文件更名为给定参数 File 指定的路径名 |
8.4.1 获取文件属性
java
复制代码
在 Java 中获取文件属性信息的第一步是先创建一个 File 类对象并指向一个已存在的文件, 然后调用表 1 中的方法进行操作。
java
复制代码
示例:
假设有一个文件位于 C:\windows\notepad.exe。编写 Java 程序获取并显示该文件的长度、是否可写、最后修改日期以及文件路径等属性信息。实现代码如下:
public class Test02 {
public static void main(String[] args) {
String path = "C:/windows/"; // 指定文件所在的目录
File f = new File(path, "notepad.exe"); // 建立File变量,并设定由f变量引用
System.out.println("C:\\windows\\notepad.exe文件信息如下:");
System.out.println("============================================");
System.out.println("文件长度:" + f.length() + "字节");
System.out.println("文件或者目录:" + (f.isFile() ? "是文件" : "不是文件"));
System.out.println("文件或者目录:" + (f.isDirectory() ? "是目录" : "不是目录"));
System.out.println("是否可读:" + (f.canRead() ? "可读取" : "不可读取"));
System.out.println("是否可写:" + (f.canWrite() ? "可写入" : "不可写入"));
System.out.println("是否隐藏:" + (f.isHidden() ? "是隐藏文件" : "不是隐藏文件"));
System.out.println("最后修改日期:" + new Date(f.lastModified()));
System.out.println("文件名称:" + f.getName());
System.out.println("文件路径:" + f.getPath());
System.out.println("绝对路径:" + f.getAbsolutePath());
}
}
在上述代码中 File 类构造方法的第一个参数指定文件所在位置,这里使用C:/作为文件的实际路径;第二个参数指定文件名称。创建的 File 类对象为 f,然后通过 f 调用方法获取相应的属性,最终运行效果如下所示。
8.4.2 创建和删除文件
java
复制代码
File 类不仅可以获取已知文件的属性信息,还可以在指定路径创建文件,以及删除一个文件。创建文件需要调用 createNewFile() 方法,删除文件需要调用 delete() 方法。无论是创建还是删除文件通常都先调用 exists() 方法判断文件是否存在。
示例:
假设要在 C 盘上创建一个 test.txt 文件,程序启动时会检测该文件是否存在,如果不存在则创建;如果存在则删除它再创建。实现代码如下:
public class Test03 {
public static void main(String[] args) throws IOException {
File f = new File("C:\\test.txt"); // 创建指向文件的File对象
if (f.exists()) // 判断文件是否存在
{
f.delete(); // 存在则先删除
}
f.createNewFile(); // 再创建
}
}
运行程序之后可以发现,在 C 盘中已经创建好了 test.txt 文件。但是如果在不同的操作系统中,路径的分隔符是不一样的,例如:
* Windows 中使用反斜杠\表示目录的分隔符。
* Linux 中使用正斜杠/表示目录的分隔符。
java
复制代码
程序的运行结果和前面程序一样,但是此时的程序可以在任意的操作系统中使用。代码修改如下:
public static void main(String[] args) throws IOException {
String path = "C:" + File.separator + "test.txt"; // 拼凑出可以适应操作系统的路径
File f = new File(path);
if (f.exists()) // 判断文件是否存在
{
f.delete(); // 存在则先删除
}
f.createNewFile(); // 再创建
}
注意:在操作文件时一定要使用 File.separator 表示分隔符。在程序的开发中,往往会使用 Windows 开发环境,因为在 Windows 操作系统中支持的开发工具较多,使用方便,而在程序发布时往往是直接在 Linux 或其它操作系统上部署,所以这时如果不使用 File.separator,则程序运行就有可能存在问题。关于这一点我们在以后的开发中一定要有所警惕。
8.4.3 创建和删除目录
java
复制代码
File 类除了对文件的创建和删除外,还可以创建和删除目录。创建目录需要调用 mkdir() 方法,删除目录需要调用 delete() 方法。无论是创建还是删除目录都可以调用 exists() 方法判断目录是否存在。
示例:
编写一个程序判断 C 盘根目录下是否存在 config 目录,如果存在则先删除再创建。实现代码如下:
public class Test04 {
public static void main(String[] args) {
String path = "C:/config/"; // 指定目录位置
File f = new File(path); // 创建File对象
if (f.exists()) {
f.delete();
}
f.mkdir(); // 创建目录
}
}
8.4.4 遍历目录
java
复制代码
通过遍历目录可以在指定的目录中查找文件,或者显示所有的文件列表。File 类的 list() 方法提供了遍历目录功能
8.4.4.1 String[] list()
java
复制代码
该方法表示返回由 File 对象表示目录中所有文件和子目录名称组成的字符串数组,如果调用的 File 对象不是目录,则返回 null。
提示:list() 方法返回的数组中仅包含文件名称,而不包含路径。但不保证所得数组中的相同字符串将以特定顺序出现,特别是不保证它们按字母顺序出现。
8.4.4.2 String[] list(FilenameFilter filter)
java
复制代码
该方法的作用与 list() 方法相同,不同的是返回数组中仅包含符合 filter 过滤器的文件和目录,如果 filter 为 null,则接受所有名称。
java
复制代码
假设要遍历 C 盘根目录下的所有文件和目录,并显示文件或目录名称、类型及大小。使用 list() 方法的实现代码如下:
public class Test05 {
public static void main(String[] args) {
File f = new File("C:/"); // 建立File变量,并设定由f变量变数引用
System.out.println("文件名称\t\t文件类型\t\t文件大小");
System.out.println("===================================================");
String fileList[] = f.list(); // 调用不带参数的list()方法
for (int i = 0; i < fileList.length; i++) { // 遍历返回的字符数组
System.out.print(fileList[i] + "\t\t");
System.out.print((new File("C:/", fileList[i])).isFile() ? "文件" + "\t\t" : "文件夹" + "\t\t");
System.out.println((new File("C:/", fileList[i])).length() + "字节");
}
}
}
java
复制代码
假设希望只列出目录下的某些文件,这就需要调用带过滤器参数的 list() 方法。首先需要创建文件过滤器,该过滤器必须实现 java.io.FilenameFilter 接口,并在 accept() 方法中指定允许的文件类型。如下所示为允许 SYS、TXT 和 BAK 格式文件的过滤器实现代码:
public class ImageFilter implements FilenameFilter {
// 实现 FilenameFilter 接口
@Override
public boolean accept(File dir, String name) {
// 指定允许的文件类型
return name.endsWith(".sys") || name.endsWith(".txt") || name.endsWith(".bak");
}
}
8.5 字节流使用
java
复制代码
InputStream 是 Java 所有字节输入流类的父类,OutputStream 是 Java 所有字节输出流类的父类.
使用它们的子类输入和输出字节流,包括 ByteArrayInputStream 类、ByteArrayOutputStream 类、FileInputStream 类和 FileOutputStream 类。
8.5.1 字节输入流
java
复制代码
InputStream 类及其子类的对象表示字节输入流,InputStream 类的常用子类如下。
* ByteArrayInputStream 类:将字节数组转换为字节输入流,从中读取字节。
* FileInputStream 类:从文件中读取数据。
* PipedInputStream 类:连接到一个 PipedOutputStream(管道输出流)。
* SequenceInputStream 类:将多个字节输入流串联成一个字节输入流。
* ObjectInputStream 类:将对象反序列化。
注意:在使用 mark() 方法和 reset() 方法之前,需要判断该文件系统是否支持这两个方法,以避免对程序造成影响。
方法名及返回值类型 |
说明 |
int read() |
从输入流中读取一个 8 位的字节,并把它转换为 0~255 的整数,最后返回整数。 如果返回 -1,则表示已经到了输入流的末尾。为了提高 I/O 操作的效率,建议尽量 使用 read() 方法的另外两种形式 |
int read(byte[] b) |
从输入流中读取若干字节,并把它们保存到参数 b 指定的字节数组中。 该方法返回 读取的字节数。如果返回 -1,则表示已经到了输入流的末尾 |
int read(byte[] b, int off, int len) |
从输入流中读取若干字节,并把它们保存到参数 b 指定的字节数组中。其中,off 指 定在字节数组中开始保存数据的起始下标;len 指定读取的字节数。该方法返回实际 读取的字节数。如果返回 -1,则表示已经到了输入流的末尾 |
void close() |
关闭输入流。在读操作完成后,应该关闭输入流,系统将会释放与这个输入流相关 的资源。注意,InputStream 类本身的 close() 方法不执行任何操作,但是它的许多 子类重写了 close() 方法 |
int available() |
返回可以从输入流中读取的字节数 |
long skip(long n) |
从输入流中跳过参数 n 指定数目的字节。该方法返回跳过的字节数 |
void mark(int readLimit) |
在输入流的当前位置开始设置标记,参数 readLimit 则指定了最多被设置标记的字 节数 |
boolean markSupported() |
判断当前输入流是否允许设置标记,是则返回 true,否则返回 false |
void reset() |
将输入流的指针返回到设置标记的起始处 |
8.5.2 字节输出流
java
复制代码
OutputStream 类及其子类的对象表示一个字节输出流。OutputStream 类的常用子类如下。
* ByteArrayOutputStream 类:向内存缓冲区的字节数组中写数据。
* FileOutputStream 类:向文件中写数据。
* PipedOutputStream 类:连接到一个 PipedlntputStream(管道输入流)。
* ObjectOutputStream 类:将对象序列化。
方法名及返回值类型 |
说明 |
void write(int b) |
向输出流写入一个字节。这里的参数是 int 类型,但是它允许使用表达式, 而不用强制转换成 byte 类型。为了提高 I/O 操作的效率,建议尽量使用 write() 方法的另外两种形式 |
void write(byte[] b) |
把参数 b 指定的字节数组中的所有字节写到输出流中 |
void write(byte[] b,int off,int len) |
把参数 b 指定的字节数组中的若干字节写到输出流中。其中,off 指定字节 数组中的起始下标,len 表示元素个数 |
void close() |
关闭输出流。写操作完成后,应该关闭输出流。系统将会释放与这个输出 流相关的资源。注意,OutputStream 类本身的 close() 方法不执行任何操 作,但是它的许多子类重写了 close() 方法 |
void flush() |
为了提高效率,在向输出流中写入数据时,数据一般会先保存到内存缓冲 区中,只有当缓冲区中的数据达到一定程度时,缓冲区中的数据才会被写 入输出流中。使用 flush() 方法则可以强制将缓冲区中的数据写入输出流, 并清空缓冲区 |
8.5.3 字节数组输入流
java
复制代码
ByteArrayInputStream 类可以从内存的字节数组中读取数据,该类有如下两种构造方法重载形式。
1. ByteArrayInputStream(byte[] buf):创建一个字节数组输入流,字节数组类型的数据源由参数 buf 指定。
2. ByteArrayInputStream(byte[] buf,int offse,int length):创建一个字节数组输入流,其中,参数 buf 指定字节数组类型的数据源,offset 指定在数组中开始读取数据的起始下标位置,length 指定读取的元素个数。
java
复制代码
示例:
使用 ByteArrayInputStream 类编写一个案例,实现从一个字节数组中读取数据,再转换为 int 型进行输出。代码如下:
public class test08 {
public static void main(String[] args) {
byte[] b = new byte[] { 1, -1, 25, -22, -5, 23 }; // 创建数组
ByteArrayInputStream bais = new ByteArrayInputStream(b, 0, 6); // 创建字节数组输入流
int i = bais.read(); // 从输入流中读取下一个字节,并转换成int型数据
while (i != -1) { // 如果不返回-1,则表示没有到输入流的末尾
System.out.println("原值=" + (byte) i + "\t\t\t转换为int类型=" + i);
i = bais.read(); // 读取下一个
}
}
}
8.5.4 字节数组输出流
java
复制代码
ByteArrayOutputStream 类可以向内存的字节数组中写入数据,该类的构造方法有如下两种重载形式。
1. ByteArrayOutputStream():创建一个字节数组输出流,输出流缓冲区的初始容量大小为 32 字节。
2. ByteArrayOutputStream(int size):创建一个字节数组输出流,输出流缓冲区的初始容量大小由参数 size 指定。
ByteArrayOutputStream 类中除了有前面介绍的字节输出流中的常用方法以外,还有如下两个方法。
1. intsize():返回缓冲区中的当前字节数。
2. byte[] toByteArray():以字节数组的形式返回输出流中的当前内容。
java
复制代码
示例:
使用 ByteArrayOutputStream 类编写一个案例,实现将字节数组中的数据输出,代码如下所示。
public class Test09 {
public static void main(String[] args) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] b = new byte[] { 1, -1, 25, -22, -5, 23 }; // 创建数组
baos.write(b, 0, 6); // 将字节数组b中的前4个字节元素写到输出流中
System.out.println("数组中一共包含:" + baos.size() + "字节"); // 输出缓冲区中的字节数
byte[] newByteArray = baos.toByteArray(); // 将输出流中的当前内容转换成字节数组
System.out.println(Arrays.toString(newByteArray)); // 输出数组中的内容
}
}
8.5.5 文件输入流
java
复制代码
FileInputStream 是 Java 流中比较常用的一种,它表示从文件系统的某个文件中获取输入字节。通过使用 FileInputStream 可以访问文件中的一个字节、一批字节或整个文件。
在创建 FileInputStream 类的对象时,如果找不到指定的文件将拋出 FileNotFoundException 异常,该异常必须捕获或声明拋出。
FileInputStream 常用的构造方法主要有如下两种重载形式:
1. FileInputStream(File file):通过打开一个到实际文件的连接来创建一个 FileInputStream,该文件通过文件系统中的 File 对象 file 指定。
2. FileInputStream(String name):通过打开一个到实际文件的链接来创建一个 FileInputStream,该文件通过文件系统中的路径名 name 指定。
java
复制代码
示例:FileInputStream() 两个构造方法的使用:
try {
// 以File对象作为参数创建FileInputStream对象
FileInputStream fis1 = new FileInputStream(new File("F:/mxl.txt"));
// 以字符串值作为参数创建FilelnputStream对象
FileInputStream fis2 = new FileInputStream("F:/mxl.txt");
} catch(FileNotFoundException e) {
System.out.println("指定的文件找不到!");
}
java
复制代码
示例2:使用 FileInputStream 类读取并输出该文件的内容
注意:FileInputStream 类重写了父类 InputStream 中的 read() 方法、skip() 方法、available() 方法和 close() 方法,不支持 mark() 方法和 reset() 方法。
public class Test10 {
public static void main(String[] args) {
File f = new File("D:/myJava/HelloJava.java");
FileInputStream fis = null;
try {
// 因为File没有读写的能力,所以需要有个InputStream
fis = new FileInputStream(f);
// 定义一个字节数组
byte[] bytes = new byte[1024];
int n = 0; // 得到实际读取到的字节数
System.out.println("D:\\myJava\\HelloJava.java文件内容如下:");
// 循环读取
while ((n = fis.read(bytes)) != -1) {
String s = new String(bytes, 0, n); // 将数组中从下标0到n的内容给s
System.out.println(s);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
8.5.6 文件输出流
java
复制代码
FileOutputStream 类继承自 OutputStream 类,重写和实现了父类中的所有方法。FileOutputStream 类的对象表示一个文件字节输出流,可以向流中写入一个字节或一批字节。在创建 FileOutputStream 类的对象时,如果指定的文件不存在,则创建一个新文件;如果文件已存在,则清除原文件的内容重新写入。
FileOutputStream 类的构造方法主要有如下 4 种重载形式。
1. FileOutputStream(File file):创建一个文件输出流,参数 file 指定目标文件。
2. FileOutputStream(File file,boolean append):创建一个文件输出流,参数 file 指定目标文件,append 指定是否将数据添加到目标文件的内容末尾,如果为 true,则在末尾添加;如果为 false,则覆盖原有内容;其默认值为 false。
3. FileOutputStream(String name):创建一个文件输出流,参数 name 指定目标文件的文件路径信息。
4. FileOutputStream(String name,boolean append):创建一个文件输出流,参数 name 和 append 的含义同上。
对文件输出流有如下四点说明:
1. 在 FileOutputStream 类的构造方法中指定目标文件时,目标文件可以不存在。
2. 目标文件的名称可以是任意的,例如 D:\\abc、D:\\abc.de 和 D:\\abc.de.fg 等都可以,可以使用记事本等工具打开并浏览这些文件中的内容。
3. 目标文件所在目录必须存在,否则会拋出 java.io.FileNotFoundException 异常。
4. 目标文件的名称不能是已存在的目录。例如 D 盘下已存在 Java 文件夹,那么就不能使用 Java 作为文件名,即不能使用 D:\\Java,否则抛出 java.io.FileNotFoundException 异常。
java
复制代码
示例:
public class Test11 {
public static void main(String[] args) {
FileInputStream fis = null; // 声明FileInputStream对象fis
FileOutputStream fos = null; // 声明FileOutputStream对象fos
try {
File srcFile = new File("D:/myJava/HelloJava.java");
fis = new FileInputStream(srcFile); // 实例化FileInputStream对象
File targetFile = new File("D:/myJava/HelloJava.txt"); // 创建目标文件对象,该文件不存在
fos = new FileOutputStream(targetFile); // 实例化FileOutputStream对象
byte[] bytes = new byte[1024]; // 每次读取1024字节
int i = fis.read(bytes);
while (i != -1) {
fos.write(bytes, 0, i); // 向D:\HelloJava.txt文件中写入内容
i = fis.read(bytes);
}
System.out.println("写入结束!");
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
fis.close(); // 关闭FileInputStream对象
fos.close(); // 关闭FileOutputStream对象
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
8.6 字符流使用(16位 unicode)
java
复制代码
Java 中字节流的功能十分强大,几乎可以直接或间接地处理任何类型的输入/输出操作,但利用它却不能直接操作 16 位的 Unicode 字符。这就要用到字符流。
8.6.1 字符输入流
java
复制代码
Reader 类是所有字符流输入类的父类,该类定义了许多方法,这些方法对所有子类都是有效的。
Reader 类的常用子类如下。
1. CharArrayReader 类:将字符数组转换为字符输入流,从中读取字符。
2. StringReader 类:将字符串转换为字符输入流,从中读取字符。
3. BufferedReader 类:为其他字符输入流提供读缓冲区。
4. PipedReader 类:连接到一个 PipedWriter。
5. InputStreamReader 类:将字节输入流转换为字符输入流,可以指定字符编码。
与 InputStream 类相同,在 Reader 类中也包含 close()、mark()、skip() 和 reset() 等方法,这些方法可以参考 InputStream 类的方法。
方法名及返回值类型 |
说明 |
int read() |
从输入流中读取一个字符,并把它转换为 0~65535 的整数。如果返回 -1, 则表示 已经到了输入流的末尾。为了提高 I/O 操作的效率,建议尽量使用下面两种 read() 方法 |
int read(char[] cbuf) |
从输入流中读取若干个字符,并把它们保存到参数 cbuf 指定的字符数组中。 该方 法返回读取的字符数,如果返回 -1,则表示已经到了输入流的末尾 |
int read(char[] cbuf,int off,int len) |
从输入流中读取若干个字符,并把它们保存到参数 cbuf 指定的字符数组中。其中, off 指定在字符数组中开始保存数据的起始下标,len 指定读取的字符数。该方法返 回实际读取的字符数,如果返回 -1,则表示已经到了输入流的末尾 |
8.6.2 字符输出流
java
复制代码
与 Reader 类相反,Writer 类是所有字符输出流的父类,该类中有许多方法,这些方法对继承该类的所有子类都是有效的。
Writer 类的常用子类如下。
1. CharArrayWriter 类:向内存缓冲区的字符数组写数据。
2. StringWriter 类:向内存缓冲区的字符串(StringBuffer)写数据。
3. BufferedWriter 类:为其他字符输出流提供写缓冲区。
4. PipedWriter 类:连接到一个 PipedReader。
5. OutputStreamReader 类:将字节输出流转换为字符输出流,可以指定字符编码。
注意:Writer 类所有的方法在出错的情况下都会引发 IOException 异常。关闭一个流后,再对其进行任何操作都会产生错误。
方法名及返回值类型 |
说明 |
void write(int c) |
向输出流中写入一个字符 |
void write(char[] cbuf) |
把参数 cbuf 指定的字符数组中的所有字符写到输出流中 |
void write(char[] cbuf,int off,int len) |
把参数 cbuf 指定的字符数组中的若干字符写到输出流中。其中,off 指定 字符数组中的起始下标,len 表示元素个数 |
void write(String str) |
向输出流中写入一个字符串 |
void write(String str, int off,int len) |
向输出流中写入一个字符串中的部分字符。其中,off 指定字符串中的起 始偏移量,len 表示字符个数 |
append(char c) |
将参数 c 指定的字符添加到输出流中 |
append(charSequence esq) |
将参数 esq 指定的字符序列添加到输出流中 |
append(charSequence esq,int start,int end) |
将参数 esq 指定的字符序列的子序列添加到输出流中。其中,start 指定 子序列的第一个字符的索引,end 指定子序列中最后一个字符后面的字符 的索引,也就是说子序列的内容包含 start 索引处的字符,但不包括 end 索引处的字符 |
8.6.3 字符文件输入流
java
复制代码
为了读取方便,Java 提供了用来读取字符文件的便捷类------FileReader。该类的构造方法有如下两种重载形式。
1. FileReader(File file):在给定要读取数据的文件的情况下创建一个新的 FileReader 对象。其中,file 表示要从中读取数据的文件。
2. FileReader(String fileName):在给定从中读取数据的文件名的情况下创建一个新 FileReader 对象。其中,fileName 表示要从中读取数据的文件的名称,表示的是一个文件的完整路径。
在用该类的构造方法创建 FileReader 读取对象时,默认的字符编码及字节缓冲区大小都是由系统设定的。要自己指定这些值,可以在 FilelnputStream 上构造一个 InputStreamReader。
java
复制代码
示例:
要将 D:\myJava\HelloJava.java 文件中的内容读取并输出到控制台,使用 FileReader 类的实现代码如下:
先创建了 FileReader 字符输入流对象 fr,该对象指向 D:\myJava\HelloJava.java 文件,然后定义变量 i 来接收调用 read() 方法的返回值,即读取的字符。在 while 循环中,每次读取一个字符赋给整型变量 i,直到读取到文件末尾时退出循环(当输入流读取到文件末尾时,会返回值 -1)
public class Test12 {
public static void main(String[] args) {
FileReader fr = null;
try {
fr = new FileReader("D:/myJava/HelloJava.java"); // 创建FileReader对象
int i = 0;
System.out.println("D:\\myJava\\HelloJava.java文件内容如下:");
while ((i = fr.read()) != -1) { // 循环读取
System.out.print((char) i); // 将读取的内容强制转换为char类型
}
} catch (Exception e) {
System.out.print(e);
} finally {
try {
fr.close(); // 关闭对象
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
8.6.4 字符文件输出流
java
复制代码
Java 提供了写入字符文件的便捷类------FileWriter,该类的构造方法有如下 4 种重载形式。
1. FileWriter(File file):在指定 File 对象的情况下构造一个 FileWriter 对象。其中,file 表示要写入数据的 File 对象。
2. FileWriter(File file,boolean append):在指定 File 对象的情况下构造一个 FileWriter 对象,如果 append 的值为 true,则将字节写入文件末尾,而不是写入文件开始处。
3. FileWriter(String fileName):在指定文件名的情况下构造一个 FileWriter 对象。其中,fileName 表示要写入字符的文件名,表示的是完整路径。
4. FileWriter(String fileName,boolean append):在指定文件名以及要写入文件的位置的情况下构造 FileWriter 对象。其中,append 是一个 boolean 值,如果为 true,则将数据写入文件末尾,而不是文件开始处。
FileWriter 类的创建不依赖于文件存在与否,如果关联文件不存在,则会自动生成一个新的文件。在创建文件之前,FileWriter 将在创建对象时打开它作为输出。如果试图打开一个只读文件,将引发一个 IOException 异常。
java
复制代码
示例:
首先创建了一个指向 D:\myJava\book.txt 文件的字符文件输出流对象 fw,然后使用 for 循环录入 4 个字符串,并调用 write() 方法将字符串写入到指定的文件中。最后在 finally 语句中关闭字符文件输出流。
将用户输入的 4 个字符串保存到 D:\myJava\book.txt 文件中。在这里使用 FileWriter 类中的 write() 方法循环向指定文件中写入数据,实现代码如下:
public class Test13 {
public static void main(String[] args) {
Scanner input = new Scanner(System.in);
FileWriter fw = null;
try {
fw = new FileWriter("D:\\myJava\\book.txt"); // 创建FileWriter对象
for (int i = 0; i < 4; i++) {
System.out.println("请输入第" + (i + 1) + "个字符串:");
String name = input.next(); // 读取输入的名称
fw.write(name + "\r\n"); // 循环写入文件
}
System.out.println("录入完成!");
} catch (Exception e) {
System.out.println(e.getMessage());
} finally {
try {
fw.close(); // 关闭对象
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
8.6.5 字符缓冲区输入流
java
复制代码
BufferedReader 类主要用于辅助其他字符输入流,它带有缓冲区,可以先将一批数据读到内存缓冲区。接下来的读操作就可以直接从缓冲区中获取数据,而不需要每次都从数据源读取数据并进行字符编码转换,这样就可以提高数据的读取效率。
BufferedReader 类的构造方法有如下两种重载形式。
1. BufferedReader(Reader in):创建一个 BufferedReader 来修饰参数 in 指定的字符输入流。
2. BufferedReader(Reader in,int size):创建一个 BufferedReader 来修饰参数 in 指定的字符输入流,参数 size 则用于指定缓冲区的大小,单位为字符。
java
复制代码
示例:
首先分别创建了名称为 fr 的 FileReader 对象和名称为 br 的 BufferedReader 对象,然后调用 BufferedReader 对象的 readLine() 方法逐行读取文件中的内容。如果读取的文件内容为 Null,即表明已经读取到文件尾部,此时退出循环不再进行读取操作。最后将字符文件输入流和带缓冲的字符输入流关闭。
使用 BufferedReader 类中的 readLine() 方法逐行读取 D:\myJava\Book.txt 文件中的内容,并将读取的内容在控制台中打印输出,代码如下:
public class Test13 {
public static void main(String[] args) {
FileReader fr = null;
BufferedReader br = null;
try {
fr = new FileReader("D:\\myJava\\book.txt"); // 创建 FileReader 对象
br = new BufferedReader(fr); // 创建 BufferedReader 对象
System.out.println("D:\\myJava\\book.txt 文件中的内容如下:");
String strLine = "";
while ((strLine = br.readLine()) != null) { // 循环读取每行数据
System.out.println(strLine);
}
} catch (FileNotFoundException e1) {
e1.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
fr.close(); // 关闭 FileReader 对象
br.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
8.6.6 字符缓冲区输出流
java
复制代码
BufferedWriter 类主要用于辅助其他字符输出流,它同样带有缓冲区,可以先将一批数据写入缓冲区,当缓冲区满了以后,再将缓冲区的数据一次性写到字符输出流,其目的是为了提高数据的写效率。
BufferedWriter 类的构造方法有如下两种重载形式。
1. BufferedWriter(Writer out):创建一个 BufferedWriter 来修饰参数 out 指定的字符输出流。
2. BufferedWriter(Writer out,int size):创建一个 BufferedWriter 来修饰参数 out 指定的字符输出流,参数 size 则用于指定缓冲区的大小,单位为字符。
该类除了可以给字符输出流提供缓冲区之外,还提供了一个新的方法 newLine(),该方法用于写入一个行分隔符。行分隔符字符串由系统属性 line.separator 定义,并且不一定是单个新行(\n)符。
提示:BufferedWriter 类的使用与 FileWriter 类相同,这里不再重述。
8.7 转换流
java
复制代码
InputStreamReader 用于将字节输入流转换为字符输入流,其中 OutputStreamWriter 用于将字节输出流转换为字符输出流。
java
复制代码
示例1:
在 java.txt 中输出"C语言中文网"这 6 个字,将 java.txt 保存为"UTF-8"的格式,然后通过字节流的方式读取,代码如下:
public static void main(String[] args) {
try {
FileInputStream fis = new FileInputStream("D://java.txt");
int b = 0;
while ((b = fis.read()) != -1) {
System.out.print((char) b);
}
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
输出结果为 C??????????,我们发现中文都是乱码。下面用字节数组,并通过字符串设定编码格式来显式内容,代码如下:
public static void main(String[] args) {
try {
FileInputStream fis = new FileInputStream("D://java.txt");
byte b[] = new byte[1024];
int len = 0;
while ((len = fis.read(b)) != -1) {
System.out.print(new String(b, 0, len, "UTF-8"));
}
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
这时输出结果为 C语言中文网,但是当存储的文字较多时,会出现解码不正确的问题,且字节长度无法根据解码内容自动设定,此时就需要转换流来完成。代码如下:
public static void main(String[] args) {
try {
FileInputStream fis = new FileInputStream("D://java.txt");
InputStreamReader isr = new InputStreamReader(fis, "UTF-8");
int b = 0;
while ((b = isr.read()) != -1) {
System.out.print((char) b); // 输出结果为"C语言中文网"
}
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
java
复制代码
示例2:
下面以获取键盘输入为例来介绍转换流的用法。Java 使用 System.in 代表标准输出,即键盘输入,但这个标准输入流是 InputStream 类的实例,使用不太方便,而且键盘输入内容都是文本内容,所以可以使用 InputStreamReader 将其转换成字符输入流,普通的 Reader 读取输入内容时依然不太方便,可以将普通的 Reader 再次包装成 BufferedReader,利用 BufferedReader 的 readLine() 方法可以一次读取一行内容。程序如下所示:
public static void main(String[] args) {
try {
// 将 System.in 对象转换成 Reader 对象
InputStreamReader reader = new InputStreamReader(System.in);
// 将普通的Reader 包装成 BufferedReader
BufferedReader br = new BufferedReader(reader);
String line = null;
// 利用循环方式来逐行的读取
while ((line = br.readLine()) != null) {
// 如果读取的字符串为"exit",则程序退出
if (line.equals("exit")) {
System.exit(1);
}
// 打印读取的内容
System.out.println("输入内容为:" + line);
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
8.8 JAVA示例-保存图书信息
java
复制代码
1)创建 Book 类,在该类中包含 no、name 和 price 3 个属性,分别表示图书编号、图书名称和图书单价。同时还包含两个方法 write() 和 read(),分别用于将图书信息写入到磁盘文件中和从磁盘文件中读取图书信息并打印到控制台。
此外,在 Product 类中包含有该类的 toString() 方法和带有 3 个参数的构造方法,具体的内容如下:
public class Book {
private int no; // 编号
private String name; // 名称
private double price; // 单价
public Book(int no, String name, double price) {
this.no = no;
this.name = name;
this.price = price;
}
public String toString() {
return "图书编号:" + this.no + ",图书名称:" + this.name + ",图书单价:" + this.price + "\n";
}
public static void write(List books) {
FileWriter fw = null;
try {
fw = new FileWriter("E:\\myJava\\books.txt"); // 创建FileWriter对象
for (int i = 0; i < books.size(); i++) {
fw.write(books.get(i).toString()); // 循环写入
}
} catch (Exception e) {
System.out.println(e.getMessage());
} finally {
try {
fw.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static void read() {
FileReader fr = null;
BufferedReader br = null;
try {
fr = new FileReader("E:\\myJava\\books.txt");
br = new BufferedReader(fr); // 创建BufferedReader对象
String str = "";
while ((str = br.readLine()) != null) { // 循环读取每行数据
System.out.println(str); // 输出读取的内容
}
} catch (Exception e) {
System.out.println(e.getMessage());
} finally {
try {
br.close();
fr.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
如上述代码,分别使用字符文件输出流 FileWriter 和字符缓冲区输入流 BufferedReader 完成对图书信息的存储和读取功能。
2)编写测试类 Test14,创建两个 Book 对象,并将这两个对象保存到 List 集合中,再将 List 集合对象传递给 Book 类中的 write() 方法,向 F:\product.txt 文件中写入图书信息。最后调用 Product 类中的 read() 方法读取该文件内容,代码如下:
public class Test14 {
public static void main(String[] args) {
Book book1 = new Book(1001, "C语言中文网Java教程", 159);
Book book2 = new Book(1002, "C语言中文网C++教程", 259);
List books = new ArrayList();
books.add(book1);
books.add(book2);
Book.write(books);
System.out.println("********************图书信息******************");
Book.read();
}
}