你的位置:首页 > Java教程

[Java教程][Think In Java]基础拾遗3


 目录

第十一章 持有对象
第十七章 容器深入研究
第十八章 Java I/O系统

第十一章 持有对象

1. java容器概览

java容器的两种主要类型(它们之间的主要区别在于容器中每个“槽”保存的元素个数):Collection和Map。

(1)Collection是一个独立元素的序列,这些元素都服从一条或者多条规则。Collection概括了序列的概念——一种存放一组对象的方式。

  • List:按照插入的顺序保存元素(ArrayList,LinkedList)
  • Set:不能有重复元素(HashSet,TreeSet,LinkedHasSet)
  • Queue:按照排队规则来确定对象产生的顺序(按排队时间长短即FIFO、按优先级高低)

(2)Map是一组成对的“键值对”对象,允许使用键来查找值。

  • HashMap:无序
  • TreeMap:按照比较结果的升序保存键
  • LinkedHashMap:按照插入顺序保存键

 

2. 相关工具类java.util.Arrays类和java.util.Collections类。

(1)添加一组元素

Arrays.asList();

Collections.asList();

(2)容器的打印

Arrays.toString();

 

3. 迭代器

遍历容器而不关心容器的具体类型。迭代器也是一种设计模式。(和C++中的迭代器类似)

Collection接口有一个iterator()方法返回一个Iterator对象,也就是说每个继承自Collection的容器都可以迭代(Collection接口继承自Iterable接口)。Iterator有如下方法:

boolean hasNext() :如果仍有元素可以迭代,则返回 true。 
E next() :返回迭代的下一个元素。 
void remove() :从迭代器指向的 collection 中移除迭代器返回的最后一个元素(可选操作,不是所有的容器都实现了该方法)。

 

4. ListIterator

ListIterator继承自Iterator。Iterator只能向前移动,但是ListIterator可以双向移动,ListIterator只能用于各种List。

 

5. Set

将两个相同的对象放入Set中是不行的。Set是基于对象的来确定归属性的。

HashSet:基于hash函数;TreeSet:基于红黑树;LinkedHashSet:hash函数+链表。

TreeSet 使用元素的自然顺序对元素进行排序,或者根据创建 set 时提供的 Comparator 进行排序,具体取决于使用的构造方法。

String类有一个比较器String.CASE_INSENSITIVE_ORDER;

 

6. PriorityQueue

一个基于优先级堆的无界优先级队列。优先级队列的元素按照其自然顺序进行排序,或者根据构造队列时提供的 Comparator 进行排序,具体取决于所使用的构造方法。

 

7. 简单的容器分类

 

 

第十七章 容器深入研究

1. 完整的容器分类法

下图展示了Java容器类库的完备图,包括抽象类和遗留构件(不包括Queue的实现)。

常用的容器用黑色粗线框表示,点线框表示接口,虚线框表示抽象类,实线框表示类,空心箭头表示实现关系。Produce表示任意的Map对象可以生成Collection对象,任意的Collection对象可以生成Iterator对象。

 

2. 容器总结

下面以表格的形式总结List、Set、Map接口及各实现类的特性:

 

特性

实现类

实现类特性

对放置的元素的要求

List

线性、有序的存储容器,可通过索引访问元素get(n)

ArrayList

数组实现。非同步。

 

Vector

类似ArrayList,同步。

 

LinkedList

双向链表。非同步。

 

Set

 

元素不能重复,元素必须定义equals()方法

 

HashSet

为快速查找设计的Set

元素必须定义hashCode()

TreeSet

保持次序的Set,底层为树结构

元素必须实现Comparable接口

LinkedHashSet

内部使用链表维护元素的顺序(插入的次序)

元素必须定义hashCode()

Map

保存键值对成员,Map中的所有键必须定义equals()方法

HashMap

基于哈希表的 Map 接口的实现,满足通用需求

键必须有恰当的hashCode(),如果修改了equals方法,需同时修改hashCode方法

TreeMap

默认根据自然顺序进行排序,或者根据创建映射时提供的 Comparator进行排序

键成员要求实现caparable接口,或者使用Comparator构造TreeMap。键成员一般为同一类型。

LinkedHashMap

类似于HashMap,但迭代遍历时取得“键值对”的顺序是其插入顺序或者最近最少使用的次序

与HashMap相同

IdentityHashMap

使用==取代equals()对“键值”进行比较的散列映射

成员通过==判断是否相等

WeakHashMap

弱键映射,允许释放映射所指向的对象

 

ConcurrentHashMap

线程安全的Map

 

 说明:

(1)各种Queue和Stack的行为,完全可以LinkedList提供支持,上述表格不包含Queue。

(2)但凡是和hash相关的,必然会设计到hashCode方法,因为要根据返回的哈希码去计算该对象在哈希表中的位置;但凡是和Tree相关的,必然会设计到Comparable接口,因为要涉及到排序。

(3)如果要进行大量的随机访问,就是用ArrayList;如果要经常从表中间插入或删除元素,则应该用LinkedList。遍历的时候,对于ArrayList优先选择get方式,LinkedList优先选择iterator方式。

(4)Collection继承了Iterable接口,故所有的Collection对象都可以使用foreach方式,对元素进行遍历。所有的Collection对象都hiuforeach循环可以与任何实现了Iterable接口的对象一起工作。

(5)自定义类作为HashMap/HashSet/LinkedHashMap/LinkedHashSet的键时,必须同时重写equals()和hashcode()方法。

 

3. hashcode()方法的编写步骤

 

4. HashMap的性能因子

几个术语:

(1)容量:表中的桶位数

(2)初始容量:表在创建时所拥有的桶位数。可以在HashMap和HashSet的构造器中指定初始容量。

(3)尺寸:表中当前存储的项数。

(4)负载因子:尺寸/容量。

HashMap和HashSet都有能指定负载因子的构造器,表示当负载情况达到该负载因子的水平时,容器将自动增加其容量(桶位数),实现方式是使容量大致加倍,并重新将现有对象分布到新的桶位集中(再散列)。

HashMap使用的默认负载因子是0.75。

 

5. 快速报错(fail-fast)

Java容器有一种保护机制,能够防止多个进程同时修改同一个容器的内容。Java容器类类库采用快速报错(fail-fast)机制,它会探查容器上的任何除了你的进程所进行的操作以外的所有变化,一旦它发现其他进程修改了容器,就会立刻抛出ConcurrentModificationException异常。这就是“快速报错”的意思——即,不是使用复杂的算法在事后来检查问题。

ConcurrentHashMap、CopyOnWriteArrayList和CopyOnWriteArraySet都使用了可以避免ConcurrentModificationException的技术。

 

6. 持有引用

如果想继续持有某个对象的引用,希望以后还能够访问到该对象,但是也希望能够允许GC释放它,这时候就应该使用Reference对象。这样,你就可以继续使用该对象,而在内存消耗殆尽的时候又允许释放该对象。

以Reference对象作为你和普通引用之间的媒介(代理),另外,一定不能有普通引用指向那个对象,这样就能达到上述目的。(普通引用指的是没有经过Reference对象包装过的引用。)如果GC发现某个对象通过普通引用是可获得的,该对象就不会被释放。

[这有点类似于C++中的shared_ptr的角色,并且创建Reference对象的时候也有点类似于RAAI。]

A a = new A();  //a引用(普通引用)的对象不能被回收Reference b = new Reference(new A());  //b应用的对象可以被回收,因为这个对象(往往是一大对象)一出生就被包裹了,没其他普通引用指向它,可以被回收

SoftReference、WeakReference、PhantomReference有强到弱排列,对应不同级别的“可获得性”。

 

第十八章 Java I/O系统

1.  InputStream

此抽象类是表示字节输入流的所有类的超类。

[结合linux下C语言中的文件流指针struct FILE来理解就好了。在Linux下一切IO设备都是文件,所以这样的一个结构体表示了系统中各种各样的数据源,但在Java中具体的各种数据源由InputStream的子类来表示,比如AudioInputStream, ByteArrayInputStream, FileInputStream, PipedInputStream,SequenceInputStream, StringBufferInputStream等。]

具体如下表:

 

2. OutputStream

与InputStream类似的理解。如下表

 

3. Java中的IO类与装饰器模式

Java IO类库的设计使用了装饰器模式。

先看看装饰器模式的UML图:(具体可参考维基百科)

java IO对应的UML图

FilterInputStream和FilterOutputStream起到的是抽象装饰者的角色,它们的子类是具体的装饰者。

如下表:

 

4. Reader和Writer

与面向字节IO的InputStream和OutputStream不同,Reader和Writer提供兼容Unicode与面向字符IO的功能。

设计Reader和Writer继承层次结构主要是为了国际化。因为InputStream和OutputStream仅支持8位字节流,并不能很好地处理16位的Unicode字符。

 

5. IO中的适配器类

(1)InputStreamReader:字节——>字符(2个字节)

InputStreamReader 是字节流通向字符流的桥梁:它使用指定的 charset 读取字节并将其解码为字符。它使用的字符集可以由名称指定或显式给定,或者可以接受平台默认的字符集。 

每次调用 InputStreamReader 中的一个 read() 方法都会导致从底层输入流读取一个或多个字节

为了达到最高效率,可要考虑在 BufferedReader 内包装 InputStreamReader。例如: BufferedReader in = new BufferedReader(new InputStreamReader(System.in));

(2)OutputStreamWriter:字符(2个字节)——>字节

OutputStreamWriter 是字符流通向字节流的桥梁:可使用指定的 charset 将要写入流中的字符编码成字节。它使用的字符集可以由名称指定或显式给定,否则将接受平台默认的字符集。

【记忆误区纠正】:之前一直将OutputStreamWriter记成了是将字节编码成字符。可以这样来纠正记忆:

(1)从OutputStreamWriter的构造函数来看:

OutputStreamWriter需要一个OutputStream对象,所以从形式上看就感觉是将字节流转换成了字符流,实则不然。构造函数需要OutputStream只是因为OutputStreamWriter需要借助OutputStream来完成具体的输出操作。

(2)从函数调用关系上来看:OutputStreamWriter.writer(字符char) ——> OutputStream.write(字节byte)

调用OutputStreamWriter之后我们得到的是一个Writer,而Writer的write()方法的参数都是字符。可以想象我们调用OutputStreamWriter的write()函数写入的字符,最后还得转化成字节流由内部的OutputStream对象来具体负责写出。所以这个OutputStreamWriter 类实际上是将字符编码成字节。

 

6. Java中标准IO重定向

System类的如下静态方法:setErr()、setOut()和setIn()可重定向IO,这些函数的参数的都是字符流。

 

7. 进程控制

Java中涉及到进程控制的有两个类:Process 和 ProcessBuilder。后者用来创建操作系统进程,返回一个前者的实例。

 

8. Java NIO

思想类似于Linux下的IO复用的,具体参考《NIO 入门》

 

9. 对象序列化

(1)概念:java的对象序列化就是将那些实现了Serializable接口的对象转换成一个字节序列,并能够在以后将这个字节序列完全恢复为原来的对象。

(2)为什么需要对象序列化?

对象序列化的概念加入到语言中是为了支持两种主要特性。一是Java的RMI,当向远程对象发送消息时,需要通过对象序列化来传递参数和返回值。二是对JavaBean来说,对象序列化也是必须的。需要在设计阶段保存它的状态信息,程序启动后进行后期恢复。

(3)具体步骤:让待序列化类实现一个标记接口Serializable,那么这个类的对象就是可序列化的了。

参考这篇博客