8.2.3 使用Lambda表达式遍历Iterator

Iterator在Java8中新增的forEachRemaining方法

Java8Iterator新增了一个forEachRemaining方法:

Iterator的forEachRemaining方法 描述
default void forEachRemaining(Consumer<? super E> action) Performs the given action for each remaining element until all elements have been processed or the action throws an exception.

该方法所需的参数Consumer是函数式接口,当程序调用IteratorforEachRemaining遍历集合元素时,程序会依次将集合元素传给Consumeracept方法:

Consumeraccept方法 描述
void accept(T t) Performs this operation on the given argument.

forEachRemaining方法的参数可以是Lambda表达式或者匿名内部类

因为forEachRemaining()方法参数Consumer是函数式接口,所以:

  • 可以传给该方法一个Lambda表达式作为参数,
  • 也可以传入一个匿名内部类作为参数。

不过lambda表达式比匿名内部类代码量比较少。

程序 Iterator新增的forEachRemaining方法来遍历集合

如下程序示范了使用Lambda表达式来遍历集合元素。

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

public class IteratorEach {
public static void main(String[] args) {
// 创建集合、添加元素的代码与前一个程序相同
Collection<String> collection = new HashSet<String>();
collection.add("1");
collection.add("2");
collection.add("3");
// 获取books集合对应的迭代器
Iterator<String> iterator = collection.iterator();
// 使用Lambda表达式(目标类型是Comsumer)来遍历集合元素
iterator.forEachRemaining(str -> System.out.println("Lambda表达式迭代集合元素:" + str));
System.out.println("----------------------------");
// 重新获取迭代器
iterator = collection.iterator();
iterator.forEachRemaining(new Consumer<String>() {
@Override
public void accept(String str) {
System.out.println("匿名内部类迭代集合元素:" + str);
}
});
}
}

运行效果如下

1
2
3
4
5
6
7
Lambda表达式迭代集合元素:1
Lambda表达式迭代集合元素:2
Lambda表达式迭代集合元素:3
----------------------------
匿名内部类迭代集合元素:1
匿名内部类迭代集合元素:2
匿名内部类迭代集合元素:3

8.2.2 使用Java 8增强的Iterator遍历集合元素

Iterator接口和Collection接口Map接口的区别

Iterator接口也是Java集合框架的成员,但它与Collection系列、Map系列的集合不一样:

  • Collection系列集合、Map系列集合主要用于盛装其他对象,
  • Iterator对象也被称为迭代器,Iterator主要用于遍历(即迭代访问)Collection集合中的元素

Iterator接口隐藏了各种Collection实现类的底层细节,向应用程序提供了遍历Collection集合元素的统一编程接口。

Iterator接口方法

Iterator接口里定义了如下4个方法

Iterator接口方法 描述
boolean hasNext() 如果被迭代的集合元素还没有被遍历完,则返回true
Object next() 返回集合里的下一个元素
void remove() 删除集合里上一次next方法返回的元素
void forEachRemaining(Consumer action) 这是Java 8Iterator新增的默认方法,该方法可使用Lambda表达式来遍历集合元素

程序 通过Iterator接口来遍历集合元素

下面程序示范了通过Iterator接口来遍历集合元素。

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

public class IteratorTest
{
public static void main(String[] args)
{
// 创建集合、添加元素的代码与前一个程序相同
Collection books = new HashSet();
books.add("轻量级Java EE企业应用实战");
books.add("疯狂Java讲义");
books.add("疯狂Android讲义");
// 获取books集合对应的迭代器
Iterator it = books.iterator();
while(it.hasNext())
{
// it.next()方法返回的数据类型是Object类型,因此需要强制类型转换
String book = (String)it.next();
System.out.println(book);
if (book.equals("疯狂Java讲义"))
{
// 从集合中删除上一次next方法返回的元素
it.remove();
}
// 对book变量赋值,不会改变集合元素本身
book = "测试字符串"; //①
}
System.out.println("集合中的数据:"+books);
}
}

运行效果:

1
2
3
4
疯狂Android讲义
轻量级Java EE企业应用实战
疯狂Java讲义
集合中的数据:[疯狂Android讲义, 轻量级Java EE企业应用实战]

Iterator仅用于遍历集合

从上面代码中可以看出, Iterator仅用于遍历集合, Iterator本身并不提供盛装对象的能力。如果需要创建Iterator对象,则必须有一个被迭代的集合。

Iterator必须依附于Collection对象

Iterator必须依附于Collection对象,若有一个Iterator对象,则必然有一个与之关联的Collection对象。 Iterator提供了两个方法来迭代访问Collection集合里的元素,并可通过remove()方法来删除集合中上一次next()方法返回的集合元素。

修改迭代变量的值对集合元素本身没有任何影响

当使用Iterator对集合元素进行迭代时, Iterator并不是把集合元素本身传给了迭代变量,而是把集合元素的值传给了迭代变量,所以修改迭代变量的值对集合元素本身没有任何影响

Iterator迭代时不可修改Collection集合里的元素

当使用Iterator迭代访问Collection集合元素时:

  • 不可以通过集合对象修改Collection集合里的元素,
    • 例如:不能通过集合对象来添加元素(不可以c.add(e)),也不能通过集合对象来移除元素(不可以c.remove(e))。
  • 如果一定要在迭代时删除元素,只有通过Iterator对象的remove方法才可以删除next()方法返回的集合元素.
    • 否则将会引发java.util.ConcurrentModificationException异常。

程序 使用Iterator迭代过程中 不可以通过 集合对象 来修改集合

下面程序示范了这一点。

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

public class IteratorErrorTest {
public static void main(String[] args) {
// 创建集合、添加元素的代码与前一个程序相同
Collection<String> collection = new HashSet<String>();
collection.add("1");
collection.add("2");
collection.add("3");
collection.add("4");
// 获取books集合对应的迭代器
Iterator<String> iterator = collection.iterator();
while (iterator.hasNext()) {
String book = iterator.next();
System.out.println(book);
if (book.equals("3")) {
// 使用Iterator迭代过程中,不可以通过 集合对象 修改集合
// collection.remove(book);
collection.add("5");
// 只有Iterator对象的remove方法才可以删除遍历的元素
// iterator.remove();
}
}
}
}

运行效果:

1
2
3
4
5
6
7
1
2
3
Exception in thread "main" java.util.ConcurrentModificationException
at java.base/java.util.HashMap$HashIterator.nextNode(HashMap.java:1493)
at java.base/java.util.HashMap$KeyIterator.next(HashMap.java:1516)
at IteratorErrorTest.main(IteratorErrorTest.java:14)

上面程序中collection.remove(book);这行代码位于Iterator迭代块内,也就是在Iterator迭代Collection集合过程中修改了Collection集合,所以程序将在运行时引发异常。

Iterator迭代器采用的是快速失败(fail-fast)机制,一旦在迭代过程中检测到该集合已经被修改(通常是程序中的其他线程修改),程序立即引发ConcurrentModificationException异常,而不是显示修改后的结果,这样可以避免共享资源而引发的潜在问题。

8.2.1 使用Lambda表达式遍历集合

函数式接口

所谓函数式接口就是只有一个抽象方法的接口,可以通过Lambda表达式来创建函数式接口实例.

Iterable接口的forEach方法

Java 8Iterable接口新增了一个forEach(Consumer action)默认方法,该方法的方法签名如下

Iterable的forEach方法签名 描述
default void forEach(Consumer<? super T> action) Performs the given action for each element of the Iterable until all elements have been processed or the action throws an exception.

这个forEach方法所需参数的类型Consumer是一个函数式接口
Iterable接口是Collection接口的父接口,因此Collection集合也可直接调用 继承 得到的forEach方法

Consumer函数式接口的accept方法

当程序调用IterableforEach(Consumer action)遍历集合元素时,程序会依次将集合元素传给Consumeraccept(T t)方法(accept方法是该接口中唯一的抽象方法)。

Consumer接口的抽象方法 描述
void accept(T t) Performs this operation on the given argument.

使用Lambda表达式来遍历集合

正因为Consumer是函数式接口,因此可以使用Lambda表达式来遍历集合元素.

程序示例

如下程序示范了使用Lambda表达式和匿名内部类方式来遍历集合元素。

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

public class CollectionEach {
public static void main(String[] args) {
// 创建一个集合
Collection<String> Strs = new HashSet<>();
Strs.add("1");
Strs.add("2");
Strs.add("3");
Strs.add("4");
Strs.add("5");
// 调用forEach()方法遍历集合,使用Lambda表达式方式
Strs.forEach(str -> System.out.println("Lambda表达式 遍历:" + str));
System.out.println("-----------------------");
// 调用forEach()方法遍历集合,使用匿名内部类方式:
Strs.forEach(new Consumer<String>() {
@Override
public void accept(String str) {
System.out.println("匿名内部类 遍历:" + str);
}
});
}
}

运行效果:

1
2
3
4
5
6
7
8
9
10
11
Lambda表达式 遍历:1
Lambda表达式 遍历:2
Lambda表达式 遍历:3
Lambda表达式 遍历:4
Lambda表达式 遍历:5
-----------------------
匿名内部类 遍历:1
匿名内部类 遍历:2
匿名内部类 遍历:3
匿名内部类 遍历:4
匿名内部类 遍历:5

上面程序中调用了IterableforEach默认方法来遍历集合元素,传给该方法的参数是个Lambda表达式,该Lambda表达式的目标类型是Consumer
forEach()方法会自动将集合元素逐个地传给Lambda表达式的形参,这样Lambda表达式的代码体即可遍历到集合元素了。

8.2 Collection和Iterator接口

Collection接口是ListSetQueue接口的父接口,该接口里定义的方法既可用于操作Set集合也可用于操作ListQueue集合。 Collection接口里定义了如下操作集合元素的方法

添加元素到集合的方法

方法 描述
boolean add(Object o) 该方法用于向集合里添加一个元素。如果集合对象被添加操作改变了,则返回true
boolean addAll(Collection c) 该方法把集合c里的所有元素添加到指定集合里。如果集合对象被添加操作改变了,则返回true

从集合中删除元素的方法

方法 描述
boolean remove(Object o) 删除集合中的指定元素o,当集合中包含了一个或多个元素o时,该方法只删除第一个符合条件的元素,并返回true
boolean removeAll(Collection c) 从集合中删除集合c里包含的所有元素(相当于用调用该方法的集合减集合c),如果删除了一个或一个以上的元素,则该方法返回true
boolean retainAll(Collection c) 保留集合c中出现的元素,从集合中删除集合c里不包含的元素,(相当于把调用该方法的集合变成该集合和集合c的交集),如果该操作改变了调用该方法的集合,则该方法返回true
void clear() 清除集合里的所有元素,将集合长度变为0。

查找元素的方法

方法 描述
boolean contains(Object o) 返回集合里是否包含指定元素
boolean containsAll( Collection c) 返回集合里是否包含集合c里的所有元素

集合长度相关方法

方法 描述
boolean isEmpty() 返回集合是否为空。当集合长度为0时返回true,否则返回false
int size() 该方法返回集合里元素的个数

取出迭代器或转为数组

方法 描述
Iterator iterator() 返回一个Iterator对象,用于遍历集合里的元素。
Object[] toArray() 该方法把集合转换成一个数组,所有的集合元素变成对应的数组元素。

程序 Collection集合方法介绍

下面程序示范了如何通过上面方法来操作Collection集合里的元素。

D:\Desktop\随书源码\疯狂Java讲义(第4版)光盘\codes\08\8.2\CollectionTest.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import java.util.*;

public class CollectionTest {
public static void main(String[] args) {
Collection c = new ArrayList();
// 添加元素
c.add("红楼梦");
// 虽然集合里不能放基本类型的值,但Java支持自动装箱
c.add(6);
System.out.println("c集合的元素个数为:" + c.size()); // 输出:2
// 删除指定元素
c.remove(6);
System.out.println("c集合的元素个数为:" + c.size()); // 输出:1
// 判断是否包含指定字符串
// 输出:true
System.out.println("c集合的是否包含\"红楼梦\"字符串:" + c.contains("红楼梦"));
c.add("三国演义");
System.out.println("c集合的元素:" + c); // 输出:[红楼梦, 三国演义]
Collection b = new HashSet();
b.add("三国演义");
b.add("西游记");
System.out.println("b集合的元素:" + b); // [红楼梦, 三国演义]
System.out.println("c集合是否完全包含b集合?" + c.containsAll(b)); // 输出false
// 用c集合减去b集合里的元素
// 从c集合移除b集合中的元素
c.removeAll(b);
System.out.println("c集合的元素:" + c); // [红楼梦]
// 删除c集合里所有元素
c.clear();
System.out.println("c集合的元素:" + c); // []
System.out.println("______________________");
c.add("红楼梦");
c.add("三国演义");

b.add("三国演义");
b.add("西游记");
System.out.println(c);
System.out.println(b);
// b集合只保留c集合中的元素。
// 求b集合和c集合的交集
b.retainAll(c);
System.out.println("b集合的元素:" + b);
}
}

上面程序中创建了两个Collection对象,一个是c集合,一个是books集合,其中c集合是ArrayList,而books集合是HashSet()虽然它们使用的实现类不同,但当把它们当成Collection来使用时,使用add,remove,clear等方法来操作集合元素时没有任何区别。

编译和运行上面程序,看到如下运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
c集合的元素个数为:2
c集合的元素个数为:1
c集合的是否包含"红楼梦"字符串:true
c集合的元素:[红楼梦, 三国演义]
b集合的元素:[三国演义, 西游记]
c集合是否完全包含b集合?false
c集合的元素:[红楼梦]
c集合的元素:[]
______________________
[红楼梦, 三国演义]
[三国演义, 西游记]
b集合的元素:[三国演义]

总结

Collection的用法有: 添加元素删除元素返回元素个数 以及清空整个集合等。
当使用System.outprintln()方法来输出集合对象时,将输出[e1,e2,...]的形式,这显然是因为所有的Collection实现类都重写了toString()方法,该方法可以一次性地输出集合中的所有元素。
如果想依次访问集合里的每一个元素,则需要使用某种方式来遍历集合元素,下面介绍遍历集合元素的两种方法。

8.1 Java集合概述

数组无法存放数量变化的数据

数组长度不可变化,一旦在初始化数组时指定了数组长度,这个数组长度就是不可变的,如果需要保存数量变化的数据,数组就有点无能为力了

集合有什么用

为了保存数量不确定的数据,以及保存具有映射关系的数据(也被称为关联数组),Java提供了集合类。集合类主要负责保存、盛装其他数据,因此集合类也被称为容器类。

集合类的包

起初所有的集合类都位于java.util包下,后来为了处理多线程环境下的并发安全问题,Java 5还在java.util.concurrent包下提供了一些多线程支持的集合类。

集合类与数组的区别

  • 数组元素既可以是基本类型的值,也可以是对象的引用变量;
  • 集合里不能保存基本类型的值,集合里只能保存对象的引用变量。

集合类的两个根接口

Java的集合类主要由两个接口派生而出: CollectionMap,CollectionMapJava集合框架的根接口,这两个接口又包含了一些子接口或实现类。

如图8.1所示是Collection接口、子接口及其实现类的继承树。
这里有一张图片

Collection的子接口

图8.1显示了Collection体系里的集合,其中粗线圈出的

Set,List,Queue这三个接口是Collection接口派生的三个子接口,

  • Set代表了无序不可重复集合,
  • List代表有序可重复集合;
  • QueueJava提供的队列实现,有点类似于List

如图8.2所示是Map体系的继承树,所有的Map实现类用于保存具有映射关系的数据。
这里有一张图片
图8.2显示了Map接口的众多实现类,这些实现类在功能、用法上存在一定的差异,但它们都有个功能特征:

Map接口

Map保存的每项数据都是key-value对,也就是由keyvalue两个值组成。
Map里的key是不可重复的,
key用于标识集合里的每项数据,如果需要查阅Map中的数据时,总是根据Mapkey来获取。

对于图8.1和图8.2中粗线标识的4个接口,

  • 把一个对象添加到Set集合时,Set集合无法记住添加这个元素的顺序,所以Set里的元素不能重复,否则系统无法准确识别这个元素;
  • List集合可以记住每次添加元素的顺序、且List的长度可变。
  • Map集合里面的每项数据都由两个值组成。

图8.3显示了这三种集合的示意图。
这里有一张图片

访问各个集合中元素的区别

  • 如果访问List集合中的元素,可以直接根据元素的索引来访问;
  • 如果访问Set集合中的元素,则只能根据元素本身来访问(这也是Set集合里元素不允许重复的原因)。
  • 如果访问Map集合中的元素,可以根据每项元素的key来访问其value;

集合的常见实现类

对于SetListQueueMap四种集合,最常用的实现类,分别是HashSetTreeSetArrayListArrayDequeLinkedListHashMapTreeMap这些实现类。
注意
本章主要讲解没有涉及并发控制的集合类,对于Java 5新増的具有并发控制的集合类,以及Java 7新增的TransferQueue及其实现类LinkedTransferQueue,将在笫16章与多线程一起介绍。

第8章 Java集合 本章要点

  • 集合的概念和作用
  • 使用Lambda表达式遍历集合
  • Collection集合的常规用法
  • 使用Predicate操作集合
  • 使用Iteratorforeach循环遍历Collection集合
  • HashSetLinkedHashSet的用法
  • 对集合使用Stream进行流式编程
  • EnumSet的用法
  • TreeSet的用法
  • ArrayListVector
  • List集合的常规用法
  • Queue接口与Deque接口
  • 固定长度的List集合
  • ArrayDeque的用法
  • PriorityQueue的用法
  • Map的概念和常规用法
  • LinkedList集合的用法
  • TreeMap的用法
  • HashMapHashTable
  • 几种特殊的Map实现类
  • Hash算法对HashSetHashMap性能的影响
  • Collections工具类的用法
  • Java9新增的不可变集合
  • Enumeration迭代器的用法
  • Java的集合体系

java集合用途

  • Java集合类是一种特别有用的工具类,可用于存储数量不等的对象,
  • 并可以实现常用的数据结构,如队列等。
  • Java集合还可用于保存具有映射关系的关联数组

java集合分类

Java集合大致可分为SetListQueueMap四种体系,其中

  • Set代表无序不可重复的集合;
  • List代表有序可以重复的集合;
  • Map代表具有映射关系的集合,
  • Java 5增加了Queue体系集合,代表一种队列集合实现。

java集合中来存放对象的引用

Java集合就像一种容器,可以把多个对象的引用”丢进”该容器中,虽然集合中存放的是对象的引用,但习惯上也以认为存放的是对象。

Java5对集合的增强

Java 5之前,Java集合会丢失容器中所有对象的数据类型,把所有对象都当成Object类型处理;
Java5增加了泛型以后,Java集合可以记住容器中对象的数据类型,从而可以编写出更简洁、健壮的代码。
本章不会介绍泛型的知识,本章重点介绍Java的4种集合体系的功能和用法。

本章重点

本章将详细介绍Java的4种集合体系的常规功能,深入介绍各集合实现类所提供的独特功能,深入分析各实现类的实现机制,以及用法上的细微差别,并给出不同应用场景选择哪种集合实现类的建议。

15.1.2 文件过滤器

File类中使用到FilenameFilter接口的方法

方法 描述
String[] list(FilenameFilter filter) Returns an array of strings naming the files and directories in the directory denoted by this abstract pathname that satisfy the specified filter.
File[] listFiles(FilenameFilter filter) Returns an array of abstract pathnames denoting the files and directories in the directory denoted by this abstract pathname that satisfy the specified filter.

File类的list()方法中可以接收一个FilenameFilter参数,通过该参数可以只列出符合条件的文件。

这里的FilenameFilter接口和javax.swing.filechooser包下的FileFilter抽象类的功能非常相似,可以把FileFilter当成FilenameFilter的实现类,但可能Sun在设计它们时产生了一些小小遗漏,所以没有让FileFilter实现FilenameFilter接口。

FilenameFilter接口方法

FilenameFilter接口里包含了一个accept方法:

FilenameFilter接口方法 描述
boolean accept(File dir, String name) Tests if a specified file should be included in a file list.

accept方法将依次对指定File的所有子目录或者文件进行迭代,如果该子目录或者文件经过accept方法处理后返回true,则list()方法会列出该子目录或者文件。

FilenameFilter接口是函数式接口

FilenameFilter接口内只有一个抽象方法,因此该接口也是一个函数式接口,可使用Lambda表达式创建实现该接口的对象

程序示例

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

public class FilenameFilterTest {
public static void main(String[] args) {
File file = new File(".");
// 使用Lambda表达式(目标类型为FilenameFilter)实现文件过滤器。
// 如果文件名以.java结尾,或者文件对应一个路径,返回true
String[] nameList = file.list((dir, name) ->
name.endsWith(".java") || new File(name).isDirectory());
for (String name : nameList) {
System.out.println(name);
}
}
}

上面程序中通过实现accept()方法,来指定哪些文件应该由list()方法列出。
运行上面程序,将看到当前路径下所有的.java文件以及文件夹被列出。

FileFilter接口和FilenameFilter接口的异同

相同点

都用于过滤文件,都只有一个方法,都是函数式接口

方法的参数列表不同

FilenameFilter接口方法 描述
boolean accept(File dir, String name) Tests if a specified file should be included in a file list.
FileFilter接口方法 描述
boolean accept(File pathname) Tests whether or not the specified abstract pathname should be included in a pathname list.

范围不同

File类对象使用FilenameFilter既可以过滤得到字符串数组,也可以得到File数组

方法 描述
String[] list(FilenameFilter filter) Returns an array of strings naming the files and directories in the directory denoted by this abstract pathname that satisfy the specified filter.
File[] listFiles(FilenameFilter filter) Returns an array of abstract pathnames denoting the files and directories in the directory denoted by this abstract pathname that satisfy the specified filter.

File类对象使用FileFilter只能得到File数组

方法 描述
File[] listFiles(FileFilter filter) Returns an array of abstract pathnames denoting the files and directories in the directory denoted by this abstract pathname that satisfy the specified filter.

所以FilenameFileter范围比较广一下

15.1 File类

File类是java.io包下代表与平台无关的文件和目录,也就是说,在程序中操作文件和目录都可以通过File类来完成。值得指出的是,不管是文件还是目录都是使用File来操作的,File能新建、删除、重命名文件和目录,

File不能访问文件内容

File不能访问文件内容本身。如果需要访问文件内容本身,则需要使用输入/输出流。

15.1.1 访问文件和目录

File类可以使用文件路径字符串来创建File实例,该文件路径字符串既可以是绝对路径,也可以是相对路径。

相对路径的处理方式

在默认情况下,系统总是依据用户的工作路径来解释相对路径,这个路径由系统属性user.dir指定,通常也就是运行Java虚拟机时所在的路径。

File类常用方法

一旦创建了File对象后,就可以调用File对象的方法来访问,File类提供了很多方法来操作文件和目录,下面列出一些比较常用的方法。

获取文件名

方法 描述
String getName() 返回此File对象所表示的文件名或路径名(如果是路径,则返回最后一级子路径名).
String getParent() 返回此File对象所对应目录(最后一级子目录)的父目录名。

获取路径名

方法 描述
String getPath() 返回次File对象所对应的路径名.
String getAbsolutePath() 返回此File对象的绝对路径。
String getCanonicalPath() 返回此File对象的的规范路径名字符串。

getPath getAbsolutePath getCanonicalPath三个方法的区别

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

public class FileTest2 {
public static void main(String[] args) {
try {
File tmpFile = File.createTempFile("Hello_", "_World");
System.out.println(tmpFile.getName());
System.out.println(tmpFile.getPath());
System.out.println(tmpFile.getCanonicalPath());
System.out.println(tmpFile.getAbsolutePath());
tmpFile.deleteOnExit();
System.out.println("-------------------------------");
File file = new File(".");
System.out.println("getPath=" + file.getPath());
System.out.println("getCanonicalPath=" + file.getCanonicalPath());
System.out.println("getAbsolutePath=" + file.getAbsolutePath());
System.out.println("-------------------------------");
File testFile = new File("./test.txt");
System.out.println("getPath=" + testFile.getPath());
System.out.println("getCanonicalPath=" + testFile.getCanonicalPath());
System.out.println("getAbsolutePath=" + testFile.getAbsolutePath());

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

}
}

运行效果:

1
2
3
4
5
6
7
8
9
10
11
12
Hello_6147365813433103955_World
C:\Users\lan\AppData\Local\Temp\Hello_6147365813433103955_World
C:\Users\lan\AppData\Local\Temp\Hello_6147365813433103955_World
C:\Users\lan\AppData\Local\Temp\Hello_6147365813433103955_World
-------------------------------
getPath=.
getCanonicalPath=G:\Desktop\随书源码\疯狂Java讲义(第4版)光盘\codes\15\15.1
getAbsolutePath=G:\Desktop\随书源码\疯狂Java讲义(第4版)光盘\codes\15\15.1\.
-------------------------------
getPath=.\test.txt
getCanonicalPath=G:\Desktop\随书源码\疯狂Java讲义(第4版)光盘\codes\15\15.1\test.txt
getAbsolutePath=G:\Desktop\随书源码\疯狂Java讲义(第4版)光盘\codes\15\15.1\.\test.txt

总结

方法 描述
getPath() 方法返回构造时传入的路径
getAbsolutePath() 方法返回文件的绝对路径,
  • 如果构造的时候是全路径就直接返回全路径,
  • 如果构造时是相对路径,就返回当前目录的路径 + 构造File对象时的路径
getCanonicalPath() 方法返回绝对路径,会...这两个相对路径符号删除掉

重命名

方法 描述
boolean renameTo(File dest) 重命名此File对象所对应的文件或目录,如果重命名成功,则返回true;否则返回false.

判断方法

方法 描述
boolean exists() 判断File对象所对应的文件或目录是否存在
boolean canWrite() 判断File对象所对应的文件和目录是否可写
boolean canRead() 判断File对象所对应的文件和目录是否可读
boolean isFile() 判断File对象所对应的是否是文件,而不是目录。
boolean isDirectory() 判断File对象所对应的是否是目录,而不是文件。
boolean isAbsolute() 判断File对象所对应的文件或目录是否是绝对路径
该方法消除了不同平台的差异,可以直接判断File对象是否为绝对路径。
  • UNIX/Linux/BSD等系统上,如果路径名开头是一条斜线(/),则表明该File对象对应一个绝对路径;
  • Windows等系统上,如果路径开头是盘符,则说明它是一个绝对路径。

获取常规文件信息

方法 描述
long lastModified() 返回文件的最后修改时间
long length() 返回文件内容的长度

创建文件

方法 描述
boolean createNewFile() 当此File对象所对应的文件不存在时,该方法将新建一个该File对象所指定的新文件,如果创建成功则返回true;否则返回false.
static File createTempFile(String prefix,String suffix) 默认的临时文件目录中创建一个临时的空文件
  • 使用给定前缀、系统生成的随机数和给定后缀作为文件名。
  • prefix参数必须至少是3字节长。建议前缀使用一个短的、有意义的字符串,比如”hjb或”"mail".
  • suffix参数可以为null,在这种情况下,将使用默认的后缀”.tmp
static File createTempFile(String prefix,String suffix,File directory) directory所指定的目录中创建一个临时的空文件,使用给定前缀系统生成的随机数和给定后缀作为文件名。

删除文件

方法 描述
boolean delete() 删除File对象所对应的文件或目录
void deleteOnExit() 注册一个删除钩子,指定当Java虚拟机退出时,删除File对象所对应的文件和目录。

创建目录

方法 描述
boolean mkdir() 试图创建一个File对象所对应的目录,如果创建成功,则返回true;否则返回false。调用该方法时File对象必须对应一个路径,而不是一个文件。

查询目录列表

方法 描述
String[] list() 列出File对象的所有子文件名和子路径名,返回String数组。
File[] listFiles() 列出File对象的所有子文件和子路径,返回File数组。
static File[] listRoots() 列出系统所有的根路径。这是一个静态方法,可以直接通过File类来调用。

程序示例

上面详细列出了File类的常用方法,下面程序以几个简单方法来测试一下File类的功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import java.io.*;

public class FileTest {
public static void main(String[] args) throws IOException {
// 以当前路径来创建一个File对象
File file = new File(".");
// 直接获取文件名,输出一点
System.out.println("file.getName()="+file.getName());
// 获取相对路径的父路径可能出错,下面代码输出null
System.out.println("file.getParent()="+file.getParent());
// 获取绝对路径
System.out.println("file.getAbsoluteFile()="+file.getAbsoluteFile());
// 获取上一级路径
System.out.println("file.getAbsoluteFile().getParent()="+file.getAbsoluteFile().getParent());
// 在当前路径下创建一个临时文件
File tmpFile = File.createTempFile("aaa", ".txt", file);
// 指定当JVM退出时删除该文件
tmpFile.deleteOnExit();
// 以系统当前时间作为新文件名来创建新文件
File newFile = new File(System.currentTimeMillis() + "");
System.out.println("newFile对象是否存在:" + newFile.exists());
// 以指定newFile对象来创建一个文件
newFile.createNewFile();
// 以newFile对象来创建一个目录,因为newFile已经存在,
// 所以下面方法返回false,即无法创建该目录
newFile.mkdir();
// 使用list()方法来列出当前路径下的所有文件和路径
String[] fileList = file.list();
System.out.println("====当前路径下所有文件和路径如下====");
for (String fileName : fileList) {
System.out.println(fileName);
}
// listRoots()静态方法列出所有的磁盘根路径。
File[] roots = File.listRoots();
System.out.println("====系统所有根路径如下====");
for (File root : roots) {
System.out.println(root);
}
}
}

运行上面程序,效果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
file.getName()=.
file.getParent()=null
file.getAbsoluteFile()=G:\Desktop\随书源码\疯狂Java讲义(第4版)光盘\codes\15\15.1\.
file.getAbsoluteFile().getParent()=G:\Desktop\随书源码\疯狂Java讲义(第4版)光盘\codes\15\15.1
newFile对象是否存在:false
====当前路径下所有文件和路径如下====
1583829365214
aaa5846229108561415065.txt
FilenameFilterTest.java
FileTest.java
====系统所有根路径如下====
C:\
D:\
E:\
F:\
G:\
H:\

可以看到程序列出当前路径的所有文件和路径时,列出了程序创建的临时文件,但程序运行结束后,aaa.txt临时文件并不存在,因为程序指定虚拟机退出时自动删除该文件。

反斜线作为分隔符的情况

Windows的路径分隔符使用反斜线(\),而Java程序中的反斜线表示转义字符,所以如果需要在Windows的路径下包括反斜线,则应该使用两条反斜线,如F:\\abc\\test.txt

建议使用斜线作为分隔符

或者直接使用斜线(/)也可以,Java程序支持将斜线(/)当成平台无关的路径分隔符.

13.10 本章小结

本章从标准的SQL语句讲起,简单介绍了关系数据库的基本理论及标准的SQL语句的相关语法,包括DDLDML、简单査询语句、多表连接査询和子査询语句。本章重点讲解了JDBC数据库访问的详细步骤,包括加载数据库驱动,获取数据库连接,执行SQL语句,处理执行结果等。
本章在介绍JDBC数据库访问时详细讲解了StatementPreparedStatementCallableStatement的区别和联系,并介绍了如何处理数据表的Blob列。本章还介绍了事务相关知识,包括如何在标准的SQL语句中进行事务控制和在JDBC编程中进行事务控制。本章最后介绍了如何利用DatabaseMetaData、系统表来分析数据库信息,并讲解了数据源的原理和作用,示范了两个开源数据源实现的用法。

13.9 使用连接池管理连接

数据库连接的建立及关闭是极耗费系统资源的操作,在多层结构的应用环境中,这种资源的耗费对系统性能影响尤为明显。通过前面介绍的方式(通过DriverManager获取连接)获得的数据库连接,个数据库连接对象均对应一个物理数据库连接,每次操作都打开一个物理连接,使用完后立即关闭连接。频繁地打开、关闭连接将造成系统性能低下
数据库连接池的解决方案是:当应用程序启动时,系统主动建立足够的数据库连接,并将这些连接组成一个连接池。每次应用程序请求数据库连接时,无须重新打开连接,而是从连接池中取出已有的连接使用,使用完后不再关闭数据库连接,而是直接将连接归还给连接池。通过使用连接池,将大大提高程序的运行效率
对于共享资源的情况,有一个通用的设计模式:资源池(Resource pool),用于解决资源的频繁请求、释放所造成的性能下降。为了解决数据库连接的频繁请求、释放,JDBC2.0规范引入了数据库连接池技术。数据库连接池是Connection对象的工厂。数据库连接池的常用参数如下。

  • 数据库的初始连接数。
  • 连接池的最大连接数。
  • 连接池的最小连接数。
  • 连接池每次增加的容量。

JDBC的数据库连接池使用javax.sql.DataSource来表示, DataSource只是一个接口,该接口通常由商用服务器(如WebLogicWebSphere)等提供实现,也有一些开源组织提供实现(如DBCPC3PO等)。
提示:
DataSource通常被称为数据源,它包含连接池和连接池管理两个部分,但习惯上也经常把DataSource称为连接池。

13.9.1 DBCP数据源

DBCPApache软件基金组织下的开源连接池实现,该连接池依赖该组织下的另一个开源系统common-pool如果需要使用该连接池实现,则应在系统中增加如下两个jar文件。

  • commons-dbcp. jar:连接池的实现。
  • commons-pool jar:连接池实现的依赖库。

登录http://commons.apache.org/站点即可下载commons-pool.zipcommons-dbcp.zip两个压缩文件解压缩这两个文件即可得到上面提到的两个JAR文件。为了在程序中使用这两个JAR文件,应该把它们添加到系统的类加载路径中(比如添加到 CLASSPATH环境变量中)
Tomcat的连接池正是采用该连接池实现的。数据库连接池既可以与应用服务器整合使用,也可以由应用程序独立使用。下面的代码片段示范了使用DBCP来获得数据库连接的方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//创建数据源对象
BasicDataSource ds= new BasicDataSource();
//设置连接池所需的驱动
ds.setDriverclassName("com.mysql.jdbc.Driver");
//设置连接数据库的URL
ds. setUrl("jdbc:mysql://localhost:3306/javaee");
//设置连接数据库的用户名
ds.setUsername("root");
//设置连接数据库的密码
ds.setPassword("pass")
//设置连接池的初始连接数
ds.setInitialsize(5);
//设置连接池最多可有多少个活动连接数
ds.setMaxActive(20);
//设置连接池中最少有2个空闲的连接
ds.setMinIdle(2)

数据源和数据库连接不同,数据源无须创建多个,它是产生数据库连接的工厂,因此整个应用只需要一个数据源即可。也就是说,对于一个应用,上面代码只要执行一次即可。建议把上面程序中的ds设置成static成员变量,并且在应用开始时立即初始化数据源对象,程序中所有需要获取数据库连接的地方直接访问该ds对象,并获取数据库连接即可。通过DataSource获取数据库连接的代码示例如下:

1
2
//通过数据源获取数据库连接
Connection conn=ds.getConnection();

当数据库访问结束后,程序还是像以前一样关闭数据库连接,如下代码所示:

1
2
//释放数据库连接
conn.close();

但上面代码并没有关闭数据库的物理连接,它仅仅把数据库连接释放,归还给连接池,让其他客端可以使用该连接。

13.9.2 C3P0数据源

相比之下,C3P0数据源性能更胜一筹, Hibernate就推荐使用该连接池。C3P0连接池不仅可以自动清理不再使用的Connection,还可以自动清理StatementResultsetC3P0连接池需要版本为1.3以上的JRE,推荐使用1.4以上的JRE。如果需要使用C3P0连接池,则应在系统中增加如下JAR文件。

  • c3p0-0.9.1.2jar:C3PO连接池的实现。

下载C3P0数据源

登录C2P0站点即可下载C3P0数据源的最新版本,下载后得到一个c3p0-0.91.2bin.zip文件(版本号可能有区别),解压缩该文件,即可得到上面提到的JAR文件。
下面代码通过C3P0连接池获得数据库连接。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//创建连接池实例
ComboPooledDataSource ds=new ComboPooledDataSource();
//设置连接池连接数据库所需的驱动
ds.setDriverClass("com.mysql.jdbc.Driver");
//设置连接数据库的URL
ds.setJdbcUrl("jdbc:mysql://localhost:3306/javaee");
//设置连接数据库的用户名
ds.setUser("root");
//设置连接数据库的密码
ds.setPassword("32147");
//设置连接池的最大连接数
ds.setMaxPoolSize(40);
//设置连接池的最小连接数
ds.setMinPoolSize(2);
//设置连接池的初始连接数
ds.setInitialPoolSize(10);
//设置连接池的缓存Statement的最大数
ds.setMaxstatements(180);

在程序中创建C3P0连接池的方法与前面介绍的创建DBCP连接池的方法基本类似,此处不再解释。一旦获取了C3P0连接池之后,程序同样可以通过如下代码来获取数据库连接。

1
2
//获得数据库连接
Conection conn=ds.getConnection();