Java核心技术一

数据类型

整形

  • long类型后可加后缀 ”L”或”l” 表示
  • 二进制可加前缀 ”0b”或”0B” 表示
  • 八进制可加前缀 ”0” 表示
  • 十六进制可加前缀 ”0x”或”0X”表示
  • Java 7开始可为数字字面量加下划线使之更易读:1_000_000_000,编译器会去除

    Java中没有任何无符号形式的整形

    浮点类型

    类型 存储需求 取值范围
    float 4字节 大约3.402 823 47E+38F  (有效位数为6~7位)
    double 8字节 大约1.797 693 134 862 315 70E+308  (有效位数为15位)
  • double表示双精度,一般都采用double类型,除非需要单精度的库或需要存储大量数据
  • float类型后加后缀 ”F”或”f” 表示;无后缀的默认为double类型

    可用十六进制表示浮点数值,用p表示指数,而不是e

下面是表示溢出和出错情况的三个特殊的浮点数值 (实际开发中很少用到):

Double.POSITIVE_INFINITY Double.NEGATIVE_INFINITY Double.NaN
正无穷大 负无穷大 NaN(不是一个数字)

可以使用Double.isNaN方法检查是不是一个数字

char 类型

  • 表示单个字符;也可以用一个或两个char表示Unicode字符

特殊字符的转义序列:

转义序列 名称 Unicode值
\b 退格 \u0008
\t 制表 \u0009
\n 换行 \u000a
\r 回车 \u000d
\‘’ 双引号 \u0022
\‘ 单引号 \u0027
\\ 反斜杠 \u005c

Unicode转义序列会在解析代码之前得到处理

boolean类型

  • boolean类型有两个值:true、false
  • 整形值和布尔值之间不能进行相互转换

    2. 变量

  • 变量名必须是一个以字母开头 并由字母或数字构成的序列,字母包括Unicode字符
  • 可以使用Character类的isJavaIdentifierStart和isJavaIdentifierPart方法来检查是否为Unicode字符

    常量

  • 常量用关键字 final 表示,常量名全大写
  • final 表示这个变量只能被赋值一次,而后不能更改
  • 类常量:可以在一个类的多个方法中使用,定义在main方法外部,可以使用关键字static final表示

    运算符

  • 整数被0除将会产生异常,浮点数被0除将会得到 无穷大NaN 的结果

    Math数学函数与常量

  • sqrt(x):平方根
  • pow(x, a):乘方
  • Math.PI:
  • Math.e:e常量的近似值
  • floor: 返回不大于的最大整数
  • round: 表示“四舍五入”,算法为Math.floor(x+0.5),即将原来的数字加上0.5后再向下取整。(正数小数点后大于5则进位;负数小数点后小于以及等于5都舍去,大于5的则进位)
  • ceil: 不小于它的最小整数

    floorModfloorDiv 旨在解决整型余数问题。在java中,如果n为负数,n%2为-1,而floorMod(position + adjustment, 12)总是返回一个0到11之间的数字。(对于负的除数,还是会返回负值)。floorDiv则返回0到12之间的数字。

    自增自减运算

  • 前缀:先运算再使用数值
  • 后缀:先使用数值再运算

    位运算符

    &:and 与
    |:or 或
    ^:xor 异或
    ~:not 非
    >> 或 <<:位模式左移或右移
    >>>:用0填充高位

    运算符级别

    运算符 结合性
    [].()(方法调用) 从左向右
    ! ~ ++ – + -(一元运算) ()(强制类型转换) new 从右向左
    * / % 从左向右
    + - 从左向右
    <<>> >>> 从左向右
    < <= > >= instanceof 从左向右
    == != 从左向右
    & 从左向右
    ^ 从左向右
    | 从左向右
    && 从左向右
    || 从左向右
    ? : 从右向左
    = += -= *= /= %= |= ^= <<= >>= >>>= 从右向左
    ### 字符串

字符串

Java没有内置的字符串类型,而是在标准Java类库中提供了预定义类:String,且String为final不可变,因此总是线程安全的,一般都用它作为参数传递

  • substring(a, b):截取字符串
  • + :连接字符串 (每次连接,都会构建一个新的String对象,效率低占空间,使用StringBuilder类,通过apped添加字符串可避免此问题)
  • join(“/“, “a”, “b”, “c”):分隔字符串 “a/b/c”
  • equal:检测两个字符串是否相等
    “==”运算符只能检测两个字符串是否放在同一位置
  • equalIgnoreCase:忽略大小写检测是否相等
  • compareTo(String str):按照字典顺序检测一个字符串是否在str前后位置,之前为负,之后为正,相等为0
  • replace(oldStr, newStr):用newStr代替原始字符串中 所有的oldStr,并返回一个新字符串
  • toLowerCase() / toUpperCase():更改大小写,并返回一个新串
  • trim():删除原始字符串头部和尾部的空格

String类没有提供用于修改字符串的方法,如果想修改,可先substring截取需要的字符,再拼接替换的字符串
不可变字符串的优点:编译器可以让字符串共享(复制原始字符串的效率远高于截取拼接,用StringBuilder可以高效拼接字符串)

StringBuilder类中常用方法:
StringBuilder append(String str):追加一个字符串并返回this
void setCharAt(int i, char c):将第i个位置设为c
StringBuilder insert(int offset, String str):在offset位置插入一个字符串并返回this
StringBuilder delete(int startIndex, int endIndex):删除偏移量从startIndex到-endIndex-1的字符并返回this
toString():返回一个与构建器或缓冲器内容相同的字符串

空串 和 null

  • 空串””:是长度为0的字符串。可用str.length()==0 或 str.equals(“”)检测
  • null:表示目前没有任何对象与该变量关联

    检查一个字符串既不是null也不是空串
    if(str != null && str.length() != 0)

    String / StringBuilder / StringBuffer 的区别

    执行速度快慢为:StringBuilder > StringBuffer > String

因为String为字符串常量,而StringBuilderStringBuffer均为字符串变量,即String对象一旦创建之后该对象是不可更改的,但另外两者的对象是变量,是可以更改的。

在线程安全上,StringBuilder是线程不安全的,而StringBuffer是线程安全的,String为final不可变总是安全的:

如果一个StringBuffer对象在字符串缓冲区被多个线程使用时,StringBuffer中很多方法可以带有synchronized关键字,所以可以保证线程是安全的,但StringBuilder的方法则没有该关键字,所以不能保证线程安全,有可能会出现一些错误的操作。所以如果要进行的操作是多线程的,那么就要使用StringBuffer,但是在单线程的情况下,还是建议使用速度比较快的StringBuilder。

String:适用于少量的字符串操作的情况
StringBuilder:适用于单线程下在字符缓冲区进行大量操作的情况
StringBuffer:适用多线程下在字符缓冲区进行大量操作的情况

输入输出

  • Java SE 6 引入了Console类实现读取密码:

    Console cons = new Console();
    String username = cons.realLine("username: "); 
    char[] passwd = cons.readPassword("Password: ");    //显示Password: 并读取输入

安全起见,返回的密码存放在一维字符数组中,而不是字符串中 (在对密码进行处理后,应该马上用一个填充值覆盖数组元素)

格式化输入

  • Java SE 5.0 引用了C库中的printf方法:

用于printf的转换符

转换符 类型 举例
d 十进制整数 77
x 十六进制整数 9f
o 八进制整数 237
f 定点浮点数 15.7
e 指数浮点数 1.57e+01
g 通用浮点数 ——
a 十六进制浮点数 0x1.fccdp3
s 字符串 Hello
c 字符 H
b 布尔 True
h 散列码 42628b2
tx or Tx 日期时间 已过时,现用java.time类
% 百分号 %
n 与平台有关的行分隔符 ——

**用于printf的标志

标志 目的 举例
+ 打印正数负数的符号 +333.33
空格 在正数之前添加空格 | 333.33|
0 数字前面补0 00333.33
- 左对齐 |333.33 |
( 将负数括在括号内 (333.33)
, 添加分组分隔符 3,333,33
#(对于f格式) 包含小数点 3,333.
#(对于x或0格式) 添加前缀0x或0 0xcafe
$ 给定被格式化的参数索引:%1$x将以十六进制打印第一个参数 159 9F
< 格式化前面说明的数值:%d%<x 以十进制和十六进制打印同一个数值 159 9F
- 可以使用静态的String.format方法创建一个格式化的字符串,而不打印输出
#### 文件输入与输出
- 读取文件:用File对象构造一个Scanner对象:
in
1
2
- **写入文件**:构造一个PrintWriter对象(如果文件不存在,创建该文件):  
```PrintWriter out = new PrintWriter("myfile.txt", "UTF-8");

PrintWriter(String fileName):构造一个将数据写入文件的PrintWriter
static Path get(String pathName):根据给定的路径名构造一个Path

控制流程

控制流程语句

  • Switch中,case标签可以是:类型为char、byte、short、int的常量表达式,枚举常量字符串字面量(Java SE 7)

  • 带标签的break:标签必须放在要跳出的最外层循环之前,且紧跟一个冒号”:”.

    大数值BigInteger / BigDecimal

  • java.math包中的两个类,可以处理包含任意长度数字序列的数值

  • 使用BigInteger.valueOf方法可以将普通的数值转换为大数值:BigInteger a = new BigInteger.valueOf(100);

  • 不能使用+/等处理大数值,而是使用 *add / multiply** 方法。

    BigInteger add(BigInteger other
    BigInteger subtract(BigInteger other
    BigInteger multiply(BigInteger other)
    BIgInteger divide(BigInteger other)
    BigInteger mod(BigInteger other) 返回两个大整数的和、差、积、商、余数。
    int compareTo(BigInteger other) 两个大整数相等,返回0;小于返回负数,大于返回正数
    static BigInteger valueOf(long x) 返回值等于x的大整数
    static BigDecimal valueOf(long x, int scale) 返回值为x或 x / 10 ^ scale的一个大实数

    数组 foreach循环

  • 遍历数组中的每个元素

  • Arrays.toString(str):打印一维数组中的所有值(更简单的遍历打印方法)

  • Arrays.deepToString(str):打印二维数组中的数据元素

    匿名数组:new int[] {} 可以在不创建新变量的情况下重新初始化一个数组
    在返回值为数组的方法中,如果结果为0,则可以创建一个长度0的数组:new arrName[0]。数组长度为0与null不同。

  • Arrays.copyOf(str, str.length) :将一个数组的所有值拷贝到一个新的数组中去,第二个参数是数组长度,通常用来增加数组的大小

    命令行参数

    public static void main(String[] args){}
    以java className -g cruel world 的形式运行程序:args数组将包含:
    args[0]: “-g”
    args[1]: “cruel”
    args[2]:”world”

    数组排序

    随机不重复抽取一个范围内的数值:
    将数组最后一个值赋给之前抽取的数的索引,并将总数减去 1 ,则抽取不会重复

  • static int binarySearch(type[] a, int start, int end, type v):采用二分搜索发查找值

  • static void fill(type[] a, type v):将数组的所有数据元素设置为v

  • static boolean equals(type[] a, type[] b):如果两个数组大小相同,且下标相同的元素都对应相等,返回true


对象与类

三个主要特性

  • 对象的行为(behavior):可以对对象施加哪些操作或方法
  • 对象的状态(state):施加方法时,对象如何响应
  • 对象标识(identity):如何辨别具有相同行为与状态的不同对象

一个对象变量并没有实际包含一个对象,而仅仅引用一个对象
任何对象变量的都是对存储在另外一个地方的一个对象的引用,new操作符的返回值也是一个引用

类之间的关系

  • 依赖(“uses-a”):一个类的方法操作另一个类的对象
  • 聚合(“has-a”):类A 的对象包含 类B 的对象
  • 继承(“is-a”):类A 扩展 类B,继承 类B的方法
    更改器方法 与 访问器方法
  • 更改器方法:调用后可以改变对象的状态 ,setter
  • 访问器方法:只访问对象而不修改对象 ,getter
构造器
  • 构造器总是伴随着new操作符的执行被调用,而不能对一个已经存在的对象调用构造器来达到重新设置实力域的目的

  • 构造器与类同名

  • 每个类可以有一个以上的构造器

  • 构造器可以有0个、1个或多个参数

  • 构造器没有返回值

  • 所有Java的对象都是在堆中构造的

  • 不要在构造器中定义与实例域重名的局部变量

    隐式参数和显式参数

    public void raiseSalary(double byPercent){
    double raise = salary * byPercent / 100; //可以这样调用 double raise = this.salary * byPercent / 100;
    salary += raise; //this,salary += raise;
    }

  • raiseSalary方法有两个参数,第一个为隐式参数,是在方法名之前的类对象,第二个为显式参数,位于方法名后面括号中。

  • 隐式参数:方法调用的目标或接收者,this关键字表示隐式参数

  • 显式参数:明显地列在方法声明中

    封装的优点
  • 通过 getName()、getAge()形式的方法获取实力域值,称为访问器方法 或 域访问器。

  • name是一个只读域,在构造器中设置完毕,就没有任何方法可以对它进行修改,这样确保name域不会受到外界的破坏

    在需要获得或设置实力域的值时,应提供:
    一个私有的数据域
    一个公有的域访问器方法
    一个公有的域更改器方法

  • 优点:可以改变内部实现,除了该类的方法之外,不会影响其他代码;更改器方法可以执行错误检查

  • 注意:不要编写返回引用可变对象的访问器方法,否则会破坏封装性。

  • 如果需要返回一个可变数据域的拷贝,就应该使用clone。

    私有方法

    在实现一个类时,所有数据域都应该设置为私有的,大部分方法都设计为公有的。公有方法不能随意删除,其他代码可能依赖于它。
    如果希望将一个计算代码划分成若干个独立的辅助方法,或者需要一个特别的协议以及一个特别的调用次序,可以将方法设为private的。(P111)

    final实力域
    • 可以将实力域定义为final,构建对象时必须初始化这样的域,且不可更改;
    • final大都应用于基本类型域,或不可变类的域(类中每个方法都不会改变其对象,如String类)
      静态域与静态方法
  • 静态域:如果域定义为static,则一个类中只有一个这样的域;

  • 静态方法:一种不能向对象实施操作的方法,或没有this的方法(在非静态方法中,this表示这个方法的隐式参数)

  • 使用静态方法:一个方法不需要访问对象状态,其所需参数都是通过显式参数提供(如:Math.pow)
                         一个方法只需要访问类的静态域。

    工厂方法

    静态方法还有另一常见用途,使用静态工厂方法来构造对象。
    如:NumberFormat类

    NumberFormat currencyFormatter = NumberFormat.getCurrencyInstance();
    NumberFormat percentFormat = NumberFormat.getPercentInstance();
    double x = 0.1;
    syso.print(currencyFormatter.format(x)) //prints $0.10
    syso.print(percentFormatter.fotmat(x)); //prints 10%

    不使用构造器完成以上操作的原因:无法命名构造器,构造器的名字必须与类名相同,这里希望将得到的货币实力和百分比实例采用不同的名字。
    当使用构造器时,无法改变所构造的对象类型。而工厂方法将返回一个DecimalFormat类对象。

    main方法
  • main方法不对任何对象进行操作,在启动程序时还没有任何一个对象,静态的main方法将执行并创建程序所需要的对象。

    方法参数
  • 按值调用:表示方法接收的是调用者提供的值。(Java采用按值调用,方法得到的是所有参数值的一个拷贝)

  • 按引用调用:表示方法接收的是调用者提供的变量地址。

  • 方法得到的是对象引用的拷贝,对象引用及其他的拷贝同时引用同一个对象(对象引用是按值传递的)

    Java中方法参数的使用情况:
    1.一个方法不能修改一个基本数据类型的参数(即数值型或布尔型)

    private int s = 0;
    public void changeParameter(int a) {a++;}
    public static void main(String[] args) {
        t.changeParameter(t.s);
        System.out.println(t.s);
    }

    2.一个方法可以改变一个对象参数的状态
    3.一个方法不能让对象参数引用一个新的对象


    对象构造

    重载
  • 重载: 发生在同一个类中,方法名必须相同,参数类型不同、个数不同、顺序不同,方法返回值和访问修饰符可以不同,发生在编译时。   

  • 重写: 发生在父子类中,方法签名(方法名字和参数列表)必须相同,返回值范围小于等于父类,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类;如果父类方法访问修饰符为 private 则子类就不能重写该方法。

    默认域初始化
  • 在构造器中没有显式地给域赋予初值,就会被自动地赋为默认值(数值为0,boolean为false,对象引用为null),而方法中的局部变量必须明确地初始化。

    参数名

    一般在参数前面加个“a”, 或者 参数变量用相同的名字将实例域屏蔽,但可以采用 this隐式参数 来访问实例域

    初始化块
  • 初始化数据域的方法:在构造器中设置值, 或 在声明中赋值

  • 或 初始化块:数据域在对象初始化块中被初始化,先运行初始化块,再运行构造器的主体.(通常直接将初始化块代码放入构造器)

    ``` {
         id = nextId;
         nextId++;
        ``` }
  • 使用静态初始化块对静态域进行初始化

        ...
    }

    在类第一次加载时,将会初始化静态域

    finalize方法
  • 在垃圾回收器清除之前调用,回收资源,不要用来回收任何短缺资源,因为很难知道什么时候才能够调用

  • 确保finalizer方法在Java关闭前调用:使用Runtime.addShutdownHook添加“关闭钩”。

  • 某个资源在使用完毕后要立刻关闭,可以使用close()方法

  • 在两个包中有相同名称的类时,不能使用 * 来导入,可以在类名前面加上完整包名。

  • 编译器在编译源文件时不检查目录结构

    注释
  • @param:变量描述

  • @return:描述

  • @throws:类描述

  • @author:姓名

  • @version:版本

  • @since:文本

  • @deprecated:文本,添加一个不再使用的注释

  • @see:引用,超链接

类的设计技巧

1.保证数据私有
2.对数据初始化
3.不要在类中使用过多的基本类型
4.不是所有的域都需要独立的域访问器和更改器
5.将职责过多的类进行分解
6.类名和方法名应体现它们的职责
7.优先使用不可变的类

继承

  • 一个对象变量可以指示多种实际类型的现象被称为 多态
  • 在运行时能够自动地选择调用哪个方法的现象称为 动态绑定(默认的处理方式)
  • 不允许扩展的类称为final类,不可被继承
  • 强制转换:应先用if(Obj instanceof Obj)判断是否能够成功转换
  • 只能在继承层次内进行类型转换

动态绑定的重要特性:无需对现存代码进行修改,就可以对程序进行扩展。
理解方法调用:

1.编译器查看对象的声明类型和方法名。
2.编译器查看调用方法时提供的参数类型。如果在所有名为上述的方法中存在一个与提供的参数类型完全匹配,就选择这个方法。此过程称为重载解析
3.如果是private、static、final方法或构造器,编译器将可以准确地知道应该调用哪个方法。这种调用方式为静态绑定
4.当程序运行,并且采用动态绑定调用方法时,虚拟机一定调用与x所引对象的实际类型最合适的那个类的方法。

调用e.getSalary()方法的解析过程

1.虚拟机提取e的实际类型的放发表。既可能时Employee、Manager方法表,也可能时Employee类的其他子类的方法表。
2.虚拟机搜索定义getSalary签名的类。此时虚拟机已经知道应该调用哪个方法。
3.虚拟机调用方法。

#### 抽象类 abstract 1.包含一个或多个抽象方法的类必须为抽象的,但抽象类可以不包含抽象方法 2.抽象类可以包含具体的数据和方法,但不能被实例化 3.抽象类充当占位角色,具体实现 在子类中 4.扩展抽象类的两种方法:在抽象类中定义部分抽象方法或不定义抽象方法,则子类必须为抽象类;定义全部的抽象方法,则子类就不是抽象的 5.可以定义一个抽象类的对象变量,但只能引用非抽象子类的对象:Person p = new Student(); #### == 和 equals **==** : 它的作用是判断两个对象的地址是不是相等。即,判断两个对象是不是同一个对象。(基本数据类型==比较的是值,引用数据类型==比较的是内存地址)

equals() : 它的作用也是判断两个对象是否相等。但它一般有两种使用情况:

  • 情况1:类没有覆盖 equals() 方法。则通过 equals() 比较该类的两个对象时,等价于通过“==”比较这两个对象。
  • 情况2:类覆盖了 equals() 方法。一般,我们都覆盖 equals() 方法来两个对象的内容相等;若它们的内容相等,则返回 true (即,认为这两个对象相等)。

举个例子:

java
public class test1 {
public static void main(String[] args) {
String a = new String("ab"); // a 为一个引用
String b = new String("ab"); // b为另一个引用,对象的内容一样
String aa = "ab"; // 放在常量池中
String bb = "ab"; // 从常量池中查找
if (aa == bb) // true
System.out.println("aa==bb");
if (a == b) // false,非同一对象
System.out.println("a==b");
if (a.equals(b)) // true
System.out.println("aEQb");
if (42 == 42.0) { // true
System.out.println("true"); }}}

说明:

  • String 中的 equals 方法是被重写过的,因为 object 的 equals 方法是比较的对象的内存地址,而 String 的 equals 方法比较的是对象的值。

  • 当创建 String 类型的对象时,虚拟机会在常量池中查找有没有已经存在的值和要创建的值相同的对象,如果有就把它赋给当前引用。如果没有就在常量池中重新创建一个 String 对象。

    hashCode

  • 散列码是由对象导出的一个整型值,是没有规律的

  • hashCode方法定义在Object类中,因此每个对象都有一个默认的散列码,值为对象的存储地址。

  • StringBuilder类中没有定义hashCode方法,它的散列码是由Object类的默认hashCode方法导出的对象存储地址
    String类计算hashCode:

    int hash = 0;
    for(int i=0; i<length(); i++){
    hash = 31 * hash + charAt(i);
        }
  • 字符串内容相同,则 散列码相同

    • int hashCode() :返回对象的散列码
  • static int hash(Object…Objects) :返回一个散列码,由提供的所有对象的散列码组合得到

  • static int hashCode((int|long|double|char|boolean…) value) : 返回给定值的散列码

  • static int hashCode(type[] a) : 计算数组a的散列码

    toString

  • 只要对象与一个字符串通过操作符“+”连接,Java编译就会自动地调用toString方法,以便获得这个对象的字符串描述。

  • 强烈建议为自定义的每个类添加toString方法,便于从日志记录中获取信息和调试。

  • 如下定义

    public String toString(){
    return getClass().getName()+"[name=" + name + ",age=" + age + "]";   
    }

    泛型数组列表ArrayList<>()

  • void ensureCapacity(int capacity):确定数组存储元素数量后,分配包含capacity个内部数组

  • int size():返回数组列表中包含的实际元素数目

  • void TrimToSize():将数组列表的存储容量削减到当前尺寸(垃圾回收器回收多余的存储空间)

    使用get、set实现访问或改变数组元素的操作
    set方法只能替换数组中已经存在的元素内容

灵活扩展数组并方便访问数组元素:

ArrayList<X> list = new ArrayList<>();
while(...){
    x = ...;
list.add(x);
}
X[] a = new X[list.size()];  
list.toArray(a);       //在数组列表中间插入元素
int n = staff.size()/2;
staff.add(n, e);

自动装箱与拆箱

装箱:将基本类型用它们对应的引用类型包装起来;

拆箱:将包装类型转换为基本数据类型;

  • 自动装箱规范要求boolean、byte、char <= 127,介于-128~127之间的short和int被包装到固定的对象中。

  • 由于包装器类引用可以为null,所有自动装箱有可能抛出NullPointerException异常。

  • 装箱和拆箱是编译器认可的,而不是虚拟机

  • 包含在包装器中的内容不会改变,可以用org.omg.CORBA包中的holder类型如IntHolder、BooleanHolder等来访问存储在其中的值:

    public static void triple(IntHolder x){
        x.value = 3 * x.value;
    }
  • static int parseInt(String s[, int radix]): 返回字符串s表示的整形数值(默认十进制)。或者radix规定的进制。

  • Static Integer valueOf(String s[, int radix]):返回用s表示的整形数值进行初始化后的一个新Integer对象。或者radix规定的进制。

  • Number parse(String s):返回数字值,假设给定的String表示了一个数值。

    可变参数的方法

  • max方法可以接收任意数量的参数,Object…Object[]一样

    public static double max(double...values){
        double largest = Double.NEGATIVE_INFINITY;
        for(double v:values)
        if(v > largest) largest = v;
        return largest;
    }

    main方法可以声明为下列形式:
    public static void main(String…args)

继承的设计技巧

1.将公共操作和域放在超类;
2.不要使用受保护的域;
3.使用继承实现”is-a”关系;
4.除非所有继承的方法都有意义,否则不要使用继承;
5.再覆盖方法时,不要改变预期的行为;
6.使用多态,而非类型信息;
7.不要过多的使用反射。


接口、lambda

接口

  • 描述类具有什么功能
  • 接口没有实例
  • 实现接口的类必须实现接口中的所有方法

lambda

  • 表示可以在将来某个时间点执行的代码块的简洁方法。可以用一种简洁的方式表示使用回调或变量行为的代码。

内部类

  • 主要用于设计具有相互协作关系的类集合

代理

  • 一种实现任意接口的对象。非常专业的构造工具,可以用来构建系统级的工具

接口

  • 接口中的所有方法自动地属于public;
  • Java SE 8 之后,接口中可以提供简单的方法,且方法不能引用实例域;
  • 提供实例域和方法实现的任务应该由实现接口的那个类来完成;
  • 在实现接口时,必须把方法声明为public;
  • 接口变量必须引用实现了接口的类对象;
  • 可以使用instance检查一个对象是否实现了某个特定的接口;
  • 接口中不能包含实例域或静态方法,却可以包含常量(Java SE 8可以有静态方法);
  • 接口中的域会自动被设为public static final
  • 每个类只能继承一个类,却可以实现多个接口;
  • 接口也可以被继承扩展(extends)

1.为什么必须用接口规定类:因为Java是强类型语言,在调用方法时,编译器会检查这个方法是否存在,当一个方法中有调用某个方法,则编译器必须确认这个方法一定存在,用接口来规定就表示一定有这个方法。

2.为什么有了抽象类还需要接口: 每个类只能继承一个类,却可以实现多个接口。

接口静态方法

Java SE 8 中允许在接口中增加静态方法,但目前为止,都是将静态方法放在伴随类中。在标准库中有成对出现的接口和实用工具类,Collection/Collections或Path/Paths。(这个技术已经过时,现在可以直接在接口中实现方法。)

默认方法

可以为接口提供一个默认实现,必须用 default 修饰。

public interface Comparable<T>{
    default int compareTo(T other) {return 0;}
}                    

为什么要默认方法:因为接口中的所有方法都要实现,若只需要其中一两个方法,则可以将方法都声明为default,需要时只需覆盖即可。


解决默认方法冲突

如果在一个接口中将方法定义为默认方法,又在父类或另一个接口定义了同样的方法,则 父类优先
接口冲突:父接口提供了一个默认方法,另一接口提供了同样的方法(不论是否是默认参数),则必须覆盖这个方法来解决冲突。
当一个类同时实现两个同样的接口时,需要程序员解决二义性。

接口与回调(callback)

回调是一种常见的程序设计模式,可以指出某个特定事件发生时应该采取的动作。

public class TimerTest{
    public static void main(String[] args){
        ActionListener listener = new TimePrinter();    //构造TimePrinter类对象并转换为ActionListener
        Timer t = new Timer(10000, listener);            //将ActionListener对象传给Timer类
        t.start();    
        JOptionPane.showMessageDialog(null, "Quit program?");    //null表示在屏幕中央输出语句
        System.exit(0);
    }
}                      
class TimerPrinter implements ActionListener{
    public void actionPerformed(ActionEvent event){        //实现ActionListener接口中的方法
        System.out.println("At the tone, the time is " + new Date());
        Toolkit.getDefaultToolkit().beep();                //发出铃响
    }
}

comparator接口(P224)

当需要按长度来比较时,可以使用一个数组和一个比较器作为参数,比较器是实现了Comparator接口的实例,

public interface Comparator<T>{
    int compare(T first, T second);
}

按长度比较字符串,可以定义如下实现Comparator的类:

class LengthComparator implements Comparator<String>{
    public int compare(String first, String second){
        return first.length() - second.length();
    }
}                      

具体完成比较时,需要建立一个实例:

Comparator<String> comp = new LengthComparator();
if(comp.compare(word[i], word[j])  >  0)...

对象克隆 (Cloneable接口)

  • Cloneable接口时Java提供的一组标记接口之一。通常用来确保一个类实现一个或一组特定的方法。标记接口不包含任何方法。它的唯一作用就是允许在类型查询中使用instanceof:(建议自己的程序中不要使用标记接口)

    if(Obj instanceof Cloneable)...
  • Cloneable接口提供了一个安全的clone方法 (它没有指定clone方法,只是从Object类继承的)

  • Clone方法是Object的一个protected方法,因此代码不能直接调用这个方法。它是个浅拷贝,当原对象与克隆对象的子对象是不可变时,它是安全的。但通常子对象都是可变的,则需要重新定义一个clone方法来建立深拷贝,同时克隆所有子对象。

对于每一个类,需要确定:

1,默认的clone方法是否满足要求;
2.是否可以在可变的子对象上调用clone来修补默认的clone方法;
3.是否不该使用clone。

实际上第3个是默认选项。如果选择1、2,类必须:

1.实现Cloneable接口;
2.重新定义clone方法,并指定public访问修饰符。

  • 对于Object类的protected clone方法,虽然所有类都是Object的子类,但子类只能调用protected clone方法来克隆它自己的对象,必须重新定义clone为public才能允许所有方法克隆对象。

深拷贝

建立深拷贝,需要克隆对象中可变的实例域。

public Employee clone() throws CloneNotSupportedException{
    Employee cloned = (Employee) super.clone();
    cloned.hireDay = (Date) hireDay.clone();        //hireDay是一个Date,可变的
    return cloned;
}

!!!

  • 所有数组类型都有一个public的clone方法,而不是protected。可以用这个方法建立一个新数组,包含原数组所有元素的副本。

渐析java的浅拷贝和深拷贝

浅拷贝:使用一个已知实例对新创建实例的成员变量逐个赋值,这个方式被称为浅拷贝。

深拷贝:当一个类的拷贝构造方法,不仅要复制对象的所有非引用成员变量值,还要为引用类型的成员变量创建新的实例,并且初始化为形式参数实例值。这个方式称为深拷贝

也就是说浅拷贝只复制一个对象,传递引用,不能复制实例。而深拷贝对对象内部的引用均复制,它是创建一个新的实例,并且复制实例。

对于浅拷贝当对象的成员变量是基本数据类型时,两个对象的成员变量已有存储空间,赋值运算传递值,所以浅拷贝能够复制实例。但是当对象的成员变量是引用数据类型时,就不能实现对象的复制了。

存在一个对象Person,代码如下:
public class Asian {
private String skin;
Person person;

    public Asian(String skin,Person person){
        this.skin = skin;
        this.person = person;                    //引用赋值
    }

    public Asian(Asian asian){                 //拷贝构造方法,复制对象
        this(asian.skin,asian.person);           
    }
}

上面的对象Person有三个成员变量。name、sex、age。两个构造方法。第二个的参数为该对象,它称为拷贝构造方法,它将创建的新对象初始化为形式参数的实例值,通过它可以实现对象复制功能。

又有一个对象Asian,如下:
public class Asian {
private String skin;
Person person;

    public Asian(String skin,Person person){
        this.skin = skin;
        this.person = person;                    //引用赋值
    }

    public Asian(Asian asian){                 //拷贝构造方法,复制对象
        this(asian.skin,asian.person);           
    }
}

上面对象也存在着两个成员变量,skin 和Person对象
对于person对象有如下:

Person p1 = new Person("李四","mam",23);
Person p2 = new Person(P1);

当调用上面的语句时。P2对象将会对P1进行复制。执行情况如下如下图:

Asian a1 = new Asian("yellow",new Person("李四","mam",23));
Asian a2 = new Asian(a1);

对于Asian对象有:

New Asian(a1)执行Asian类的拷贝构造方法,由于对象赋值是引用赋值。使得a1和a2引用同一个对象

如下图:

当a1执行某条可以改变该值的语句时,那么a1将会通过这个语句也可以改变a2对象的成员变量

如果执行以下语句:


lambda

  • lambda是一个可传递的代码块,可以在以后执行一个或多次。

  • 带参数变量的表达式称为lambda表达式。

  • 即使lambda表达式没有参数,仍然要提供空括号(),就像无参方法一样;

  • 如果可以推导出一个lambda表达式的参数,则可以忽略其类型:

    Comparator<String> comp = (first, second) -> first.length() - second.length(); //因为这个lambda将赋给一个字符串比较器  
  • 如果方法只有一个参数,而且这个参数的类型可以推导出,甚至可以省略小括号:

    ActionListener listener = event -> System.out.println("...");    
  • 无需指定lambda的返回类型,会由上下文推导得出:

    (String first, String second) -> first.length() - second.length()    //可以在需要int类型结果的上下文中使用  
  • 如果一个lambda表达式只在某些分支返回一个值,而在另外一些分支不返回值,这是不合法的。

    (int x) -> {if(x >= 0) return 1;}  

    修改ActionListener接口:

    Timer t = new Timer(10000, event -> System.out.println("..."));  
    函数式接口
  • 对于只有一个抽象方法的接口,需要这个接口的对象时,就可以提供一个lambda表达式。称为函数式接口。 如:Comparator接口

    Arrays.sort(words, (first, second) -> first.length() - second.length());    //Arrays.sort的第二个参数需要一个Comparator实例  

!!!!!!!

  • 在Java中,对lambda表达式能做的也只是能转换成函数式接口
  • 不能把lambda赋给类型为Object的变量,Object不是一个函数式接口
方法引用
Timer t = new Timer(10000, System.out::println);
  • System.out::println是一个方法引用,等价于lambda表达式 x -> System.out.println(x);

  • 如果想不考虑大小写来对字符串排序:

    Arrays.sort(strings, String::compareToIgnoreCase)

    方法引用有3种情况:

  1. object :: instanceMethod
  2. Class :: staticMethod
  3. Class :: instanceMethod

    1和2等价于lambda表达式,类似于Math::pow等价于 (x, y) -> Math.pow(x, y)
    对于3,第一个参数会成为方法的目标。如:String::compareTOIgnoreCase等价于(x, y) -> x.compareToIgnoreCase(y)

类似于lambda表达式,方法引用不能独立存在,总是会转换为函数式接口的实例。

  • 可以在方法引用里使用this。如:this::equals 等同于 x -> this.equals(x)
  • 使用super也是合法的 super::instanceMethod

使用this作为目标,会调用给定方法的父类版本:

class Greeter{
    public void greet(){
        System.out.println("Hello");
    }
}

class TimedGreeter extends Greeter{
    public void greet(){
        Timer t = new Timer(1000, super::greet);    //执行super::greet方法时,会调用父类的greet方法
        t.start();
    }
}
构造器引用
  • 与方法引用类似,不过方法名为new。如:Person::new是Person构造器的一个引用
  • 可以用数组类型建立构造器引用。如:int[]::new,它有一个参数 数组长度。等价于lambda的 x -> new int[x]

    Java有一个限制,无法构造泛型类型数组,数组构造器引用可以克制这个限制。如:
    new T[n]会产生错误,因为会改为 new Object[n]
    假设需要一个Person对象数组。Stream接口有一个toArray方法可以返回Object数组:Object[] people = stream.toArray();
    不过需要的是Person引用数组,而不是Object引用数组,可以这样做:
    Person[] people = stream.toArray(Person[]::new);
    toArray方法调用这个构造器来得到一个正确类型的数组。

再谈lambda

lambda表达式有3个部分

1.一个代码块
2.参数
3.自由变量的值,这是指非参数而且不在代码中定义的变量。

关于代码块及自由变量有一个术语:闭包(closure)。在Java中,lambda表达式就是闭包。

lambda表达式可以捕获外围作用域中变量的值,不过要确保捕获的值是明确定义的。

  • lambda表达式中,只能引用不会改变的变量。若在lambda中引用变量,而变量可能在外部改变,也是不合法的。即:lambda中捕获的变量必须是最终变量
  • 在lambda中声明与一个局部变量同名的参数或局部变量是不合法的。
  • 在lambda中使用this关键字,是指创建这个lambda表达式的方法的this参数。

使用lambda表达式的重点是 延迟执行,之所以要延迟执行,有:
1. 在一个单独的线程中运行代码。
2. 多次运行代码。
3. 在算法的适当位置运行代码。(如排序的比较操作)
4. 发生某种情况时执行代码。(如点击了一个按钮,数据到达等)
5. 只在必要时才运行代码。

常用函数式接口
函数式接口 参数类型 返回类型 抽象方法名 描述 其他方法
Runnable void run 作为无参数或返回值的动作运行
Supplier T get 提供一个T类型的值
Consumer T void accept 处理一个T类型的值 andThen
BiConsumer<T,U> T, U void accept 处理T和U类型的值 andThen
Function<T, R> T R apply 有一个T类型参数的函数 compose,andThen,identity
BiFunction<T,U,R> T,U R apply 有T和U类型参数的函数 andThen
UnaryOperator T T apply 类型T上的一元操作符 compose,andThen,identity
BinaryOperator T,T T apply 类型T上的二元操作符 andThen,maxBy,minBy
Predicate T boolean test 布尔值函数 and,or,negate,isEqual
BiPredicate<T,U> T,U boolean test 有两个参数的布尔值函数 and,or,negate
##### 基本类型的函数式接口
函数式接口 参数类型 返回类型 抽象方法名
BooleanSupplier none boolean getAsBoolean
PSupplier none p getAsP
PConsumer p void accept
OjbPConsumer<T T,p void accept
PFunction<T p T apply
PToQFunction p q applyAsQ
ToPFunction<T T p applyAsP
ToPBiFunction<T,U> T,U p applyAsP
PUnaryOperator p p applyAsP
PBinaryOperator p,p p applyAsP
PPredicate p boolean test

注:p,q为int,long,double; P,Q为Int,Long,Double.

如果设计自己的接口,其中只有一个抽象方法,可以用@FunctionInterface注解来标记这个接口。

再谈 Comparator

……

内部类

  • 内部类方法可以访问该类定义所在的作用域中的数据,包括私有的数据。也可以访问创建它的外围类对象的数据域及私有数据
  • 内部类可以对同一个包中的其他类隐藏。
  • 内部类中声明的所有静态域都必须是final。因为一个静态域只有一个实例,而每个外部对象都分别有一个单独的内部类实例。如果不是final,就可能不是唯一的。
  • 内部类不能有static方法,
  • 当想要定义一个回调函数且不想编写大量代码时,使用匿名内部类比较便捷。
  • 只有内部类可以是私有类,常规类只可以是protectedpublic
    可以使用 outerObject.new InnerClass(construction parameters) 语法格式更加明确地编写内部对象的构造器。this通常可以省去。
    内部类对象的外围类引用被设置为创建内部类对象的方法中的this引用。

可以显式地命名将外围类引用设置为其他的对象。如:TimePrinter是个公有内部类,对于任意的语音时钟都可以构造一个TimePrinter:

TalkintClock jabberer = new TalkingClock(1000, true);
TalkingClock.TimePrinter listener = jabberer.new TimePrinter();

需要注意,在外围类的作用域之外,可以用OuterClass.InnerClass这样引用内部类。

内部类是一种编译器现象,与虚拟器无关。编译器会把内部类翻译成用$分隔外部类名与内部类名的常规类文件,而虚拟机对此一无所知。

局部内部类:

  • 局部内部类不能用public或private访问说明符进行声明,它的作用域被限定在声明这个局部类的块中。

  • 局部类对外部世界完全地绝对隐藏起来

  • 局部类不仅能够访问包含它们地外部类,还可以访问局部变量。但局部变量必须是final,即一旦赋值绝不会改变。
    假设想更新在一个封闭作用域内地计时器,这时final限制则不太方便。可以使用一个长度为1地数组:

    counter++; //错误
    counter[0]++; //长度为1的数组

3.2. 匿名内部类

  • 如果只创建类的一个对象,则可以不用命名。

  • 匿名类没有构造器,而会将构造器参数传递给父类构造器。在实现接口时,不能有任何参数。

    public void start(int interval, boolean beep){

    ActionListener listener = new ActionListener(){
        public void actionPerformed(ActionEvent event){
            System.out.println("" + new Date());
            if(beep) Toolkit.getDefaultToolkit().beep();  
        }
    }};
    Timer t = new Timer(interval, listener);
    t.start();

    }

一般Java程序员用匿名内部类实现事件监听器和其他回调。

双括号初始化


如果只需要传递一个数组到一个方法,之后不再需要,则可以使用匿名内部类语法:

invite(new ArrayList<String>(){{add("Tom");add("Tony")}});

外层括号建立了ArrayList的一个匿名子类,内层括号则是一个对象构造块。

生成日志或调试消息时,通常希望包含当前类名。通过 getClass() 来实现,不过getClass调用的时this.getClass(),而静态方法没有this。所以应该使用以下表达式:

new Object(){}.getClass().getEnclosingClass()    

new Object(){}会建立Object的一个匿名子类的一个匿名对象,getEnclosingClass则得到其外围类,也就是包含这个静态方法的类。

3.3. 静态内部类

  • 解决类名冲突:将类定义为另一个类的 内部公有类,可以通过 OuterClass.InnerClass访问它。如果使用内部类只是为了把一个类隐藏在另外一个类的内部,不需要内部类引用外围类对象时,则可以将内部类声明为static,以便取消产生的引用。
  • 只有内部类可以声明为static
  • 静态内部类可以有静态域和方法。

代理

  • 代理可以在运行时创建全新的类,这样的代理能够实现指定的接口。且具有下列方法:
    • 指定接口所需要的全部方法。
    • Object类中的全部方法,如 toString、equals等。
  • 然而不能再运行时定义这些方法的新代码,而是要提供一个调用处理器,调用处理器是实现了InvocationHandler接口的类对象。在这个接口中只有一个方法:Object invoke(Object proxy, Method method, Object[] args) ,无论何时调用代理对象的方法,调用处理器的invoke方法都会被调用,并向其传递Method对象和原始的调用参数。调用处理器必须给出处理调用的方式。

创建代理,需要使用Proxy类的newProxyInstance方法,此方法有三个参数 :

  • 一个类加载器,null表示默认的加载器

  • 一个Class对象数组,每个元素都是需要实现的接口。

  • 一个调用处理器。

  • 返回目录

泛型

  • 类型参数的好处:使得程序具有更好的可读性和安全性。
  • 设计泛型类型的目的:允许泛型代码和遗留代码之间能够互操作。

泛型类

定义泛型类Pair:

public class Pair<T>{
    priavte T first,second;
    public Pair() {first = null; second = null;}
    public Pair(T first, T second) {this.first = first; this.second = second;}
    public T getFirst() {return first;}
    public T getSecond() {return second;}
    public void setFirst(T newValue) {first = newValue;}
    public void setSecond(T newValue) {second = newValue;}
}
  • 一个泛型类(generic class)就是具有一个或多个类型变量的类。
  • 类型变量一般使用大写形式,且比较短。
  • 泛型类可看作普通类的工厂。用具体的类型替换类型变量就可以实例化泛型类型,如Pair,可以将结果想象成带有构造器的普通类和访问器及更改器方法。

泛型方法

定义泛型方法:

public static <T> T getMiddle(T...a){    //类型变量放在修饰符后,返回类型前。第二个T为返回类型
    return a[a.length / 2];
}
  • 泛型方法可以定义在普通类或泛型类中

    如果想知道编译器对一个泛型方法调用最终推断出哪种类型:有目的地引入一个错误,并研究所产生的错误消息。

类型变量的限定

<T extends Comparable>    //表示为实现了Comparable接口地类

为什么使用extends而不是implements?

  • 表示T应该是绑定类型的子类型。T和绑定类型可以是类,也可以是接口。
  • 一个类型变量或通配符可以有多个限定:T extends Comparable & Serializable
  • 限定类型用“&”分隔,逗号用来分隔类型变量。
  • 可以根据需要拥有多个接口父类型,但限定中至多有一个类。如果用一个类作为限定,它必须是限定列表中的第一个。

泛型代码和虚拟机

虚拟机没有泛型类型对象,在编译阶段会擦除类型变量,并用第一个限定的类型变量替换 (无限定的变量用Object)。Pair的原始类型如:

public class Pair{
    priavte Object first, second;
    public Pair(Object first, Object second){
        this.first = first; this.second = second;
    }
    ...
}
  • 为了提高效率,标签接口(如:Serializable)应放在边界列表的末尾。

翻译泛型表达式和泛型方法

程序调用泛型方法时,如果擦出返回类型,编译器会插入Employee的强制类型转换,会把这个方法调用翻译为两条虚拟机指令:

Pair<Employee> buddies = ...;
Employee buddy = buddies.getFirst();
  • 对原始方法Pair.getFirst的调用。
  • 将返回的Object类型强制转换为Employee类型。

泛型方法也进行类型擦除:

public static <T extends Comparable> T min(T[] a) 

擦除后:

public static Comparable min(Comparable[] a)

桥方法

Class DateInterval extends Pair<LocalDate>{
    public void setSecond(LocalDate second){
        if(second.compareTo(getFirst()) >= 0)
            super.setSecond(second);
    }
}

类型擦除后:

class DateInterval extends Pair{
    public void setSecond(LocalDate second) {...}
    ...
}

方法的擦除会使得存在另一个方法,即

public void setSecond(Object second)

此方法的类型参数为Object,而不是LocalDate。

DateInterval interval = new DateInterval(...);
Pair<LocalDate> pair = interval;
pair.setSecond(aDate);

这里希望对setSecond的调用具有多态性,并调用最合适的那个方法。由于pair引用DateInterval对象,所以应该调用DateInterval.setSecond。
但是类型擦除与多态发生了冲突,此时需要编译器在DateInterval类中生成一个桥方法(bridge method):

//setSecond((Date) second)为强制类型转换,使其调用的是DateInterval.setSecond(Date)方法
 public void setSecond (Object second) { setSecond((Date) second); }  
  • 变量pair已经声明为类型Pair,并且这个类型只有一个简单的方法setSecond(Object),虚拟机用pair引用的对象调用这个方法。
  • 这个对象时DateInterval类型的,因而将会调用DateInterval.setSecond(Object)方法,这个方法是合成的桥方法。它调用DateInterval.setSecond(Date)。

Java泛型转换:

  • 虚拟机中没有泛型,只有普通的类和方法。
  • 所有的类型参数都用它们的限定类型替换。
  • 桥方法被合成用来保持多态。
  • 为保持类型安全性,必要时插入强制类型转换。

约束与局限性(P321)

  1. 不能用基本类型实例化类型参数
  2. 运行时类型查询instanceof只适用于原始类型
  3. 不能创建参数化类型的数组,如 Pair[] table = new Pair[10]; //Error。但声明可以
  4. Varargs警告:向参数个数可变的方法传递一个泛型类型的实例。添加注解@SuppressWarnings(“unchecked”)或@SafeVarargs标注
  5. 不能实例化类型变量,如 public Pair() {first = new T();} //Error。类型擦出后T会改为Object
  6. 不能构造泛型数组
  7. 不能在静态域或方法中引用类型变量
  8. 不能抛出或捕获泛型类的实例,也不能扩展Throwable。但在异常规范中使用类型变量是允许的:public static void doWork(T t) throws T
  9. 可以消除对受查异常的检查。Java异常处理原则:必须为所有受查异常提供一个处理器。可以利用泛型消除这个限制
  10. 注意擦除后的冲突。泛型规范原则:要想支持擦出的转换,就需要强行限制一个类或类型变量不能同时成为两个接口类型的子类,而这两个接口是同一接口的不同参数化。

通配符类型

通配符类型中,允许类型参数变化。如:

public static void printBuddies(Pair<? extends Employee> p)

类型Pair是Pair<? extends Employee>的子类型。

Pair<manager> managerBuddies = new Pair(ceo, cfo);
Pair<? extends Employee> wildcardBuddies = managerBuddies;    //OK
wildcardBuddies.setFirst(lowlyEmployee);        //complile-time error

对setFirst的调用有一个类型错误。类型Pair<? extends Employee>方法应是这样:

? extends Employee getFirst()
void setFirst(? extends Employee)

这样不可能调用setFirst方法。编译器只知道需要某个Employee的子类型,但不知道具体是什么类型。因此它拒绝传递任何特定的类型。
但是getFirst就不存在此问题:将getFirst的返回值赋给一个Employee的引用完全合法。
这样可以区分 安全的访问器方法不安全的更改器方法

通配符的超类型限定:
? super Manager
限制为Manager的所有超类型。
带有超类型限定的通配符可以向泛型对象写入,带有子类型限定的通配符可以从泛型对象读取

无限定通配符
Pair<?>,有以下方法:

? getFirst()
void setFirst(?)  
  • getFirst的返回值只能赋给一个Object。setFirst方法不能被调用,甚至不能用Object调用。
  • Pair<?>和Pair的本质不同在于:可以用任意Object对象调用原始Pair类的setObjecgt方法。
  • 可以调用 setFirst(null)

通配符捕获(P334)

  • 通配符捕获只有在有许多限制的情况下才是合法的。
  • 编译器必须能够确信通配符表达的是单个、确定的类型。

反射和泛型

对象是泛型类的实例时,泛型类参数会被擦除,得不到太多信息。通过反射可以获得泛型类的信息。

使用Class参数进行类型匹配。

集合

集合框架图

简化图:

说明:对于以上的框架图有如下几点说明:

1.所有集合类都位于java.util包下。Java的集合类主要由两个接口派生而出:CollectionMap,Collection和Map是Java集合框架的根接口,这两个接口又包含了一些子接口或实现类。

  1. 集合接口:6个接口(短虚线表示),表示不同集合类型,是集合框架的基础。
  2. 抽象类:5个抽象类(长虚线表示),对集合接口的部分实现。可扩展为自定义集合类。
  3. 实现类:8个实现类(实线表示),对接口的具体实现。
  4. Collection 接口是一组允许重复的对象。
  5. Set 接口继承 Collection,集合元素不重复。
  6. List 接口继承 Collection,允许重复,维护元素插入顺序。
  7. Map接口是键-值对象,与Collection接口没有什么关系。
  8. Set、List和Map可以看做集合的三大类:
  • List集合是有序集合,集合中的元素可以重复,访问集合中的元素可以根据元素的索引来访问。
  • Set集合是无序集合,集合中的元素不可重复,访问集合中的元素只能根据元素本身来访问(也是集合里元素不允许重复的原因)。
  • Map集合中保存Key-value对形式的元素,访问时只能根据每项元素的key来访问其value。

    总体分析:

大致说明:
看上面的框架图,先抓住它的主干,即Collection和Map。

1、Collection是一个接口,是高度抽象出来的集合,它包含了集合的基本操作和属性。Collection包含了List和Set两大分支。

  • (1)List是一个有序的队列,每一个元素都有它的索引。第一个元素的索引值是0。List的实现类有LinkedList, ArrayList, Vector, Stack。

  • (2)Set是一个不允许有重复元素的集合。Set的实现类有HastSet和TreeSet。HashSet依赖于HashMap,它实际上是通过HashMap实现的;TreeSet依赖于TreeMap,它实际上是通过TreeMap实现的。

2、Map是一个映射接口,即key-value键值对。Map中的每一个元素包含“一个key”和“key对应的value”。AbstractMap是个抽象类,它实现了Map接口中的大部分API。而HashMap,TreeMap,WeakHashMap都是继承于AbstractMap。Hashtable虽然继承于Dictionary,但它实现了Map接口。

3、接下来,再看Iterator。它是遍历集合的工具,即我们通常通过Iterator迭代器来遍历集合。我们说Collection依赖于Iterator,是因为Collection的实现类都要实现iterator()函数,返回一个Iterator对象。**ListIterator是专门为遍历List而存在的。**

4、再看Enumeration,它是JDK 1.0引入的抽象类。作用和Iterator一样,也是遍历集合;但是Enumeration的功能要比Iterator少。在上面的框图中,Enumeration只能在Hashtable, Vector, Stack中使用。

5、最后,看Arrays和Collections。它们是操作数组、集合的两个工具类

有了上面的整体框架之后,我们接下来对每个类分别进行分析。

Collection接口

Collection接口是处理对象集合的根接口,其中定义了很多对元素进行操作的方法。Collection接口有两个主要的子接口ListSet,注意Map不是Collection的子接口,这个要牢记。

Collection接口中的方法如下:

其中,有几个比较常用的方法,比如方法add()添加一个元素到集合中,addAll()将指定集合中的所有元素添加到集合中,contains()方法检测集合中是否包含指定的元素,toArray()方法返回一个表示集合的数组。

另外,Collection中有一个iterator()函数,它的作用是返回一个Iterator接口。通常,我们通过Iterator迭代器来遍历集合。ListIterator是List接口所特有的,在List接口中,通过ListIterator()返回一个ListIterator对象

Collection接口有两个常用的子接口,下面详细介绍。

List接口

List集合代表一个有序集合,集合中每个元素都有其对应的顺序索引。List集合允许使用重复元素,可以通过索引来访问指定位置的集合元素

List接口继承于Collection接口,它可以定义一个允许重复有序集合。因为List中的元素是有序的,所以我们可以通过使用索引(元素在List中的位置,类似于数组下标)来访问List中的元素,这类似于Java的数组。

List接口为Collection直接接口。List所代表的是有序的Collection,即它用某种特定的插入顺序来维护元素顺序。用户可以对列表中每个元素的插入位置进行精确地控制,同时可以根据元素的整数索引(在列表中的位置)访问元素,并搜索列表中的元素。实现List接口的集合主要有:ArrayList、LinkedList、Vector、Stack。

ArrayList

ArrayList是一个动态数组,也是我们最常用的集合。它允许任何符合规则的元素插入甚至包括null。每一个ArrayList都有一个初始容量(10),该容量代表了数组的大小。随着容器中的元素不断增加,容器的大小也会随着增加。在每次向容器中增加元素的同时都会进行容量检查,当快溢出时,就会进行扩容操作。所以如果我们明确所插入元素的多少,最好指定一个初始容量值,避免过多的进行扩容操作而浪费时间、效率。

size、isEmpty、get、set、iterator 和 listIterator 操作都以固定时间运行。add 操作以分摊的固定时间运行,也就是说,添加 n 个元素需要 O(n) 时间(由于要考虑到扩容,所以这不只是添加元素会带来分摊固定时间开销那样简单)。

**ArrayList擅长于随机访问。同时ArrayList是非同步的。**
LinkedList

同样实现List接口的LinkedList与ArrayList不同,ArrayList是一个动态数组,而LinkedList是一个双向链表。所以它除了有ArrayList的基本操作方法外还额外提供了get,remove,insert方法在LinkedList的首部或尾部。

由于实现的方式不同,**LinkedList不能随机访问**,它所有的操作都是要按照双重链表的需要执行。在列表中索引的操作将从开头或结尾遍历列表(从靠近指定索引的一端)。这样做的好处就是可以通过较低的代价在List中进行插入和删除操作。

与ArrayList一样,**LinkedList也是非同步的**。如果多个线程同时访问一个List,则必须自己实现访问同步。一种解决方法是在创建List时构造一个同步的List: 
List list = Collections.synchronizedList(new LinkedList(...));
Vector

与ArrayList相似,但是Vector是同步的。所以说Vector是线程安全的动态数组。它的操作与ArrayList几乎一样。

Stack

Stack继承自Vector,实现一个后进先出的堆栈。Stack提供5个额外的方法使得Vector得以被当作堆栈使用。基本的push和pop 方法,还有peek方法得到栈顶的元素,empty方法测试堆栈是否为空,search方法检测一个元素在堆栈中的位置。Stack刚创建后是空栈。

Set接口

  • Set是一种不包括重复元素的Collection。它维持它自己的内部排序,所以随机访问没有任何意义。与List一样,它同样允许null的存在但是仅有一个。由于Set接口的特殊性, 所有传入Set集合中的元素都必须不同,同时要注意任何可变对象,如果在对集合中元素进行操作时,导致e1.equals(e2)==true,则必定会产生某些问题。Set接口有三个具体实现类,分别是散列集HashSet、链式散列集LinkedHashSet和树形集TreeSet。

  • Set是一种不包含重复的元素的Collection,无序,即任意的两个元素e1和e2都有e1.equals(e2)=false,Set最多有一个null元素。需要注意的是:虽然Set中元素没有顺序,但是元素在set中的位置是由该元素的HashCode决定的,其具体位置其实是固定的。

    此外需要说明一点,在set接口中的不重复是有特殊要求的。

    举一个例子:对象A和对象B,本来是不同的两个对象,正常情况下它们是能够放入到Set里面的,但是如果对象A和B的都重写了hashcode和equals方法,并且重写后的hashcode和equals方法是相同的话。那么A和B是不能同时放入到Set集合中去的,也就是Set集合中的去重和hashcode与equals方法直接相关。

为了更好地理解,请看下面的例子:

public class Test{ 
public static void main(String[] args) { 
     Set<String> set=new HashSet<String>(); 
     set.add("Hello"); 
     set.add("world"); 
     set.add("Hello"); 
     System.out.println("集合的尺寸为:"+set.size()); 
     System.out.println("集合中的元素为:"+set.toString()); 
  } 
}

运行结果:

集合的尺寸为:2
集合中的元素为:[world, Hello]

分析:由于String类中重写了hashcode和equals方法,用来比较指向的字符串对象所存储的字符串是否相等。所以这里的第二个Hello是加不进去的。

再看一个例子:

public class TestSet {

    public static void main(String[] args){

        Set<String> books = new HashSet<String>();
        //添加一个字符串对象
        books.add(new String("Struts2权威指南"));

        //再次添加一个字符串对象,
        //因为两个字符串对象通过equals方法比较相等,所以添加失败,返回false
        boolean result = books.add(new String("Struts2权威指南"));

        System.out.println(result);

        //下面输出看到集合只有一个元素
        System.out.println(books);    

    }
}

运行结果:

false
[Struts2权威指南]

说明:程序中,book集合两次添加的字符串对象明显不是一个对象(程序通过new关键字来创建字符串对象),当使用==运算符判断返回false,使用equals方法比较返回true,所以不能添加到Set集合中,最后只能输出一个元素。

HashSet
- HashSet 是一个没有重复元素的集合。它是由HashMap实现的,不保证元素的顺序(**这里所说的没有顺序是指:元素插入的顺序与输出的顺序不一致**),而且HashSet允许使用null 元素。HashSet是非同步的,如果多个线程同时访问一个哈希set,而其中至少一个线程修改了该set,那么它必须保持外部同步。** HashSet按Hash算法来存储集合的元素,因此具有很好的存取和查找性能。**

- **HashSet的实现方式大致如下,通过一个HashMap存储元素,元素是存放在HashMap的Key中,而Value统一使用一个Object对象。**

HashSet使用和理解中容易出现的误区:

- a. HashSet中存放null值  
  HashSet中是允许存入null值的,但是在HashSet中仅仅能够存入一个null值。

- b.HashSet中存储元素的位置是固定的  
  HashSet中存储的元素的是无序的,这个没什么好说的,但是由于HashSet底层是基于Hash算法实现的,使用了hashcode,所以HashSet中相应的元素的位置是固定的。

- c.必须小心操作可变对象(Mutable Object)。如果一个Set中的可变元素改变了自身状态导致Object.equals(Object)=true将导致一些问题。
LinkedHashSet
LinkedHashSet继承自HashSet,其底层是**基于LinkedHashMap来实现的**,有序,非同步。LinkedHashSet集合同样是根据元素的hashCode值来决定元素的存储位置,但是它同时使用链表维护元素的次序。这样使得元素看起来像是以插入顺序保存的,也就是说,当遍历该集合时候,**LinkedHashSet将会以元素的添加顺序访问集合的元素**。
TreeSet

TreeSet是一个有序集合,其底层是基于TreeMap实现的,非线程安全。TreeSet可以确保集合元素处于排序状态。TreeSet支持两种排序方式,自然排序和定制排序,其中自然排序为默认的排序方式。当我们构造TreeSet时,若使用不带参数的构造函数,则TreeSet的使用自然比较器;若用户需要使用自定义的比较器,则需要使用带比较器的参数。

注意:TreeSet集合不是通过hashcode和equals函数来比较元素的.它是通过compare或者comparaeTo函数来判断元素是否相等.compare函数通过判断两个对象的id,相同的id判断为重复元素,不会被加入到集合中。

Map接口

  • Map与List、Set接口不同,它是由一系列键值对组成的集合,提供了key到Value的映射。同时它也没有继承Collection。
  • 在Map中它保证了key与value之间的一一对应关系。也就是说一个key对应一个value,所以它不能存在相同的key值,当然value值可以相同。
HashMap
  • 以哈希表数据结构实现,查找对象时通过哈希函数计算其位置,它是为快速查询而设计的,其内部定义了一个hash表数组(Entry[] table),元素会通过哈希转换函数将元素的哈希地址转换成数组中存放的索引,如果有冲突,则使用散列链表的形式将所有相同哈希地址的元素串起来,可能通过查看HashMap.Entry的源码它是一个单链表结构。
LinkedHashMap
  • LinkedHashMap是HashMap的一个子类,它保留插入的顺序,如果需要输出的顺序和输入时的相同,那么就选用LinkedHashMap。

  • LinkedHashMap是Map接口的哈希表和链接列表实现,具有可预知的迭代顺序。此实现提供所有可选的映射操作,并允许使用null值和null键。此类不保证映射的顺序,特别是它不保证该顺序恒久不变。

  • LinkedHashMap实现与HashMap的不同之处在于,后者维护着一个运行于所有条目的双重链接列表。此链接列表定义了迭代顺序,该迭代顺序可以是插入顺序或者是访问顺序。

  • 根据链表中元素的顺序可以分为:按插入顺序的链表,和按访问顺序(调用get方法)的链表。默认是按插入顺序排序,如果指定按访问顺序排序,那么调用get方法后,会将这次访问的元素移至链表尾部,不断访问可以形成按访问顺序排序的链表。

    注意,此实现不是同步的。如果多个线程同时访问链接的哈希映射,而其中至少一个线程从结构上修改了该映射,则它必须保持外部同步。

    由于LinkedHashMap需要维护元素的插入顺序,因此性能略低于HashMap的性能,但在迭代访问Map里的全部元素时将有很好的性能,因为它以链表来维护内部顺序。

TreeMap
  • TreeMap 是一个有序的key-value集合,非同步,基于红黑树(Red-Black tree)实现,每一个key-value节点作为红黑树的一个节点。TreeMap存储时会进行排序的,会根据key来对key-value键值对进行排序,其中排序方式也是分为两种,一种是自然排序,一种是定制排序,具体取决于使用的构造方法。

  • 自然排序:TreeMap中所有的key必须实现Comparable接口,并且所有的key都应该是同一个类的对象,否则会报ClassCastException异常。

  • 定制排序:定义TreeMap时,创建一个comparator对象,该对象对所有的treeMap中所有的key值进行排序,采用定制排序的时候不需要TreeMap中所有的key必须实现Comparable接口。

  • TreeMap判断两个元素相等的标准:两个key通过compareTo()方法返回0,则认为这两个key相等。

如果使用自定义的类来作为TreeMap中的key值,且想让TreeMap能够良好的工作,则必须重写自定义类中的equals()方法,TreeMap中判断相等的标准是:两个key通过equals()方法返回为true,并且通过compareTo()方法比较应该返回为0。

Iterator 与 ListIterator详解

Iterator

Iterator的定义如下:

public interface Iterator<E> {}  

Iterator是一个接口,它是集合的迭代器。集合可以通过Iterator去遍历集合中的元素。Iterator提供的API接口如下:

  • boolean hasNext():判断集合里是否存在下一个元素。如果有,hasNext()方法返回 true。
  • Object next():返回集合里下一个元素。
  • void remove():删除集合里上一次next方法返回的元素。

使用示例:

public class IteratorExample {
    public static void main(String[] args) {
        ArrayList<String> a = new ArrayList<String>();
        a.add("aaa");
        a.add("bbb");
        a.add("ccc");
        System.out.println("Before iterate : " + a);
        Iterator<String> it = a.iterator();
        while (it.hasNext()) {
            String t = it.next();
            if ("bbb".equals(t)) {
                it.remove();
            }
        }
        System.out.println("After iterate : " + a);
    }
}

输出结果如下:

Before iterate : [aaa, bbb, ccc]
After iterate : [aaa, ccc] 

注意:

  • (1)Iterator只能单向移动。

  • (2)Iterator.remove()是唯一安全的方式来在迭代过程中修改集合;如果在迭代过程中以任何其它的方式修改了基本集合将会产生未知的行为。而且每调用一次next()方法,remove()方法只能被调用一次,如果违反这个规则将抛出一个异常。

ListIterator
  • ListIterator是一个功能更加强大的迭代器, 它继承于Iterator接口,只能用于各种List类型的访问。可以通过调用listIterator()方法产生一个指向List开始处的ListIterator, 还可以调用listIterator(n)方法创建一个一开始就指向列表索引为n的元素处的ListIterator.

ListIterator接口定义如下:

public interface ListIterator<E> extends Iterator<E> {
    boolean hasNext();

    E next();

    boolean hasPrevious();

    E previous();

    int nextIndex();

    int previousIndex();

    void remove();

    void set(E e);

    void add(E e);

}

由以上定义我们可以推出ListIterator可以:

  • (1)双向移动(向前/向后遍历).

  • (2)产生相对于迭代器在列表中指向的当前位置的前一个和后一个元素的索引.

  • (3)可以使用set()方法替换它访问过的最后一个元素.

  • (4)可以使用add()方法在next()方法返回的元素之前或previous()方法返回的元素之后插入一个元素.

使用示例:

public class ListIteratorExample {

    public static void main(String[] args) {
        ArrayList<String> a = new ArrayList<String>();
        a.add("aaa");
        a.add("bbb");
        a.add("ccc");
        System.out.println("Before iterate : " + a);
        ListIterator<String> it = a.listIterator();
        while (it.hasNext()) {
            System.out.println(it.next() + ", " + it.previousIndex() + ", " + it.nextIndex());
        }
        while (it.hasPrevious()) {
            System.out.print(it.previous() + " ");
        }
        System.out.println();
        it = a.listIterator(1);
        while (it.hasNext()) {
            String t = it.next();
            System.out.println(t);
            if ("ccc".equals(t)) {
                it.set("nnn");
            } else {
                it.add("kkk");
            }
        }
        System.out.println("After iterate : " + a);
    }
}

输出结果如下:

Before iterate : [aaa, bbb, ccc]
aaa, 0, 1
bbb, 1, 2
ccc, 2, 3
ccc bbb aaa 
bbb
ccc
After iterate : [aaa, bbb, kkk, nnn]

异同点

ArrayList和LinkedList
  • (1)ArrayList是实现了基于动态数组的数据结构,LinkedList基于链表的数据结构。
  • (2)对于随机访问get和set,ArrayList绝对优于LinkedList,因为LinkedList要移动指针。
  • (3)对于新增和删除操作add和remove,LinedList比较占优势,因为ArrayList要移动数据。

这一点要看实际情况的。若只对单条数据插入或删除,ArrayList的速度反而优于LinkedList。
但若是批量随机的插入删除数据,LinkedList的速度大大优于ArrayList。因为ArrayList每插入一条数据,要移动插入点及之后的所有数据。

HashTable与HashMap

相同点

  • (1)都实现了Map、Cloneable、java.io.Serializable接口。
  • (2)都是存储”键值对(key-value)”的散列表,而且都是采用拉链法实现的。

不同点

  • (1)历史原因:HashTable是基于陈旧的Dictionary类的,HashMap是Java 1.2引进的Map接口的一个实现 。

  • (2)同步性:HashTable是线程安全的,也就是说是同步的,而HashMap是线程序不安全的,不是同步的 。

  • (3)对null值的处理:HashMap的key、value都可为null,HashTable的key、value都不可为null 。

  • (4)基类不同:HashMap继承于AbstractMap,而Hashtable继承于Dictionary。

    Dictionary是一个抽象类,它直接继承于Object类,没有实现任何接口。Dictionary类是JDK 1.0的引入的。

    虽然Dictionary也支持“添加key-value键值对”、“获取value”、“获取大小”等基本操作,但它的API函数比Map少;而且Dictionary一般是通过Enumeration(枚举类)去遍历,Map则是通过Iterator(迭代M器)去遍历。

    然而由于Hashtable也实现了Map接口,所以,它即支持Enumeration遍历,也支持Iterator遍历。

    AbstractMap是一个抽象类,它实现了Map接口的绝大部分API函数;为Map的具体实现类提供了极大的便利。它是JDK 1.2新增的类。

  • (5)支持的遍历种类不同:HashMap只支持Iterator(迭代器)遍历。而Hashtable支持Iterator(迭代器)和Enumeration(枚举器)两种方式遍历。

HashMap、Hashtable、LinkedHashMap和TreeMap比较
  • Hashmap 是一个最常用的Map,它根据键的HashCode 值存储数据,根据键可以直接获取它的值,具有很快的访问速度。遍历时,取得数据的顺序是完全随机的。HashMap最多只允许一条记录的键为Null;允许多条记录的值为Null;HashMap不支持线程的同步,即任一时刻可以有多个线程同时写HashMap;可能会导致数据的不一致。如果需要同步,可以用Collections的synchronizedMap方法使HashMap具有同步的能力。

  • Hashtable 与 HashMap类似,不同的是:它不允许记录的键或者值为空;它支持线程的同步,即任一时刻只有一个线程能写Hashtable,因此也导致了Hashtale在写入时会比较慢。

  • LinkedHashMap保存了记录的插入顺序,在用Iterator遍历LinkedHashMap时,先得到的记录肯定是先插入的,也可以在构造时用带参数,按照应用次数排序。在遍历的时候会比HashMap慢,不过有种情况例外,当HashMap容量很大,实际数据较少时,遍历起来可能会比LinkedHashMap慢,因为LinkedHashMap的遍历速度只和实际数据有关,和容量无关,而HashMap的遍历速度和他的容量有关。如果需要输出的顺序和输入的相同,那么用LinkedHashMap可以实现,它还可以按读取顺序来排列,像连接池中可以应用。

    LinkedHashMap实现与HashMap的不同之处在于,后者维护着一个运行于所有条目的双重链表。此链接列表定义了迭代顺序,该迭代顺序可以是插入顺序或者是访问顺序。对于LinkedHashMap而言,它继承与HashMap、底层使用哈希表与双向链表来保存所有元素。其基本操作与父类HashMap相似,它通过重写父类相关的方法,来实现自己的链接列表特性。

    • TreeMap实现SortMap接口,内部实现是红黑树。能够把它保存的记录根据键排序默认是按键值的**升序排序,也可以指定排序的比较器,当用Iterator 遍历TreeMap时得到的记录是排过序的。TreeMap不允许key的值为null。非同步的。**
  • 一般情况下,我们用的最多的是HashMap,HashMap里面存入的键值对在取出的时候是随机的,它根据键的HashCode值存储数据,根据键可以直接获取它的值,具有很快的访问速度。在Map 中插入、删除和定位元素,HashMap 是最好的选择。
    TreeMap取出来的是排序后的键值对。但如果要按自然顺序或自定义顺序遍历键,那么TreeMap会更好。
    LinkedHashMap 是HashMap的一个子类,如果需要输出的顺序和输入的相同,那么用LinkedHashMap可以实现,它还可以按读取顺序来排列,像连接池中可以应用。

    import java.util.HashMap;
    import java.util.Iterator;
    import java.util.LinkedHashMap;
    import java.util.TreeMap;
    
    public class MapTest {
    
        public static void main(String[] args) {
    
            //HashMap
            HashMap<String,String> hashMap = new HashMap();
            hashMap.put("4", "d");
            hashMap.put("3", "c");
            hashMap.put("2", "b");
            hashMap.put("1", "a");
    
            Iterator<String> iteratorHashMap = hashMap.keySet().iterator();
    
            System.out.println("HashMap-->");
    
            while (iteratorHashMap.hasNext()){
    
                Object key1 = iteratorHashMap.next();
                System.out.println(key1 + "--" + hashMap.get(key1));
            }
    
            //LinkedHashMap
            LinkedHashMap<String,String> linkedHashMap = new LinkedHashMap();
            linkedHashMap.put("4", "d");
            linkedHashMap.put("3", "c");
            linkedHashMap.put("2", "b");
            linkedHashMap.put("1", "a");
    
            Iterator<String> iteratorLinkedHashMap = linkedHashMap.keySet().iterator();
    
            System.out.println("LinkedHashMap-->");
    
            while (iteratorLinkedHashMap.hasNext()){
    
                Object key2 = iteratorLinkedHashMap.next();
                System.out.println(key2 + "--" + linkedHashMap.get(key2));
            }
    
            //TreeMap
            TreeMap<String,String> treeMap = new TreeMap();
            treeMap.put("4", "d");
            treeMap.put("3", "c");
            treeMap.put("2", "b");
            treeMap.put("1", "a");
    
            Iterator<String> iteratorTreeMap = treeMap.keySet().iterator();
    
            System.out.println("TreeMap-->");
    
            while (iteratorTreeMap.hasNext()){
    
                Object key3 = iteratorTreeMap.next();
                System.out.println(key3 + "--" + treeMap.get(key3));
            }
    
        }
    
    }

    输出结果为:

    HashMap–>
    3–c
    2–b
    1–a
    4–d
    LinkedHashMap–>
    4–d
    3–c
    2–b
    1–a
    TreeMap–>
    1–a
    2–b
    3–c
    4–d

HashSet、LinkedHashSet、TreeSet比较
  • Set接口
    Set不允许包含相同的元素,如果试图把两个相同元素加入同一个集合中,add方法返回false。
    Set判断两个对象相同不是使用==运算符,而是根据equals方法。也就是说,只要两个对象用equals方法比较返回true,Set就不会接受这两个对象。

  • HashSet
    HashSet有以下特点:

  • 不能保证元素的排列顺序,顺序有可能发生变化。

  • 不是同步的。

  • 集合元素可以是null,但只能放入一个null。
    当向HashSet结合中存入一个元素时,HashSet会调用该对象的hashCode()方法来得到该对象的hashCode值,然后根据 hashCode值来决定该对象在HashSet中存储位置。
    简单的说,HashSet集合判断两个元素相等的标准是两个对象通过equals方法比较相等,并且两个对象的hashCode()方法返回值也相等。
    注意,如果要把一个对象放入HashSet中,重写该对象对应类的equals方法,也应该重写其hashCode()方法。其规则是如果两个对象通过equals方法比较返回true时,其hashCode也应该相同。
    另外,对象中用作equals比较标准的属性,都应该用来计算 hashCode的值。

  • LinkedHashSet
    LinkedHashSet集合同样是根据元素的hashCode值来决定元素的存储位置,但是它同时使用链表维护元素的次序。这样使得元素看起来像是以插入顺序保存的,也就是说,当遍历该集合时候,LinkedHashSet将会以元素的添加顺序访问集合的元素。
    LinkedHashSet在迭代访问Set中的全部元素时,性能比HashSet好,但是插入时性能稍微逊色于HashSet。

  • TreeSet类
    TreeSet是SortedSet接口的唯一实现类,TreeSet可以确保集合元素处于排序状态。
    TreeSet支持两种排序方式,自然排序和定制排序,其中自然排序为默认的排序方式。向TreeSet中加入的应该是同一个类的对象。
    TreeSet判断两个对象不相等的方式是两个对象通过equals方法返回false,或者通过CompareTo方法比较没有返回0。

  • 自然排序
    自然排序使用要排序元素的CompareTo(Object obj)方法来比较元素之间大小关系,然后将元素按照升序排列。
    Java提供了一个Comparable接口,该接口里定义了一个compareTo(Object obj)方法,该方法返回一个整数值,实现了该接口的对象就可以比较大小。obj1.compareTo(obj2)方法如果返回0,则说明被比较的两个对象相等,如果返回一个正数,则表明obj1大于obj2,如果是负数,则表明obj1小于obj2。如果我们将两个对象的equals方法总是返回true,则这两个对象的compareTo方法返回应该返回0。

  • 定制排序
    自然排序是根据集合元素的大小,以升序排列,如果要定制排序,应该使用Comparator接口,实现 int compare(T o1,T o2)方法。

    package com.test;  
    import java.util.HashSet;  
    import java.util.LinkedHashSet;  
    import java.util.TreeSet;  
    
    /**  
     * @description 几个set的比较  
     *    HashSet:哈希表是通过使用称为散列法的机制来存储信息的,元素并没有以某种特定顺序来存放;  
     *    LinkedHashSet:以元素插入的顺序来维护集合的链接表,允许以插入的顺序在集合中迭代;  
     *    TreeSet:提供一个使用树结构存储Set接口的实现,对象以升序顺序存储,访问和遍历的时间很快。  
     * @author Zhou-Jingxian  
     *  
     */  
    public class SetDemo {  
    
        public static void main(String[] args) {  
    
            HashSet<String> hs = new HashSet<String>();  
            hs.add("B");  
            hs.add("A");  
            hs.add("D");  
            hs.add("E");  
            hs.add("C");  
            hs.add("F");  
            System.out.println("HashSet 顺序:\n"+hs);  
    
            LinkedHashSet<String> lhs = new LinkedHashSet<String>();  
            lhs.add("B");  
            lhs.add("A");  
            lhs.add("D");  
            lhs.add("E");  
            lhs.add("C");  
            lhs.add("F");  
            System.out.println("LinkedHashSet 顺序:\n"+lhs);  
    
            TreeSet<String> ts = new TreeSet<String>();  
            ts.add("B");  
            ts.add("A");  
            ts.add("D");  
            ts.add("E");  
            ts.add("C");  
            ts.add("F");  
            System.out.println("TreeSet 顺序:\n"+ts);  
        }  
    }

    输出结果:

    HashSet 顺序:[D, E, F, A, B, C]
    LinkedHashSet 顺序:[B, A, D, E, C, F]
    TreeSet 顺序:[A, B, C, D, E, F]

    Iterator和ListIterator区别

我们在使用List,Set的时候,为了实现对其数据的遍历,我们经常使用到了Iterator(迭代器)。使用迭代器,不需要干涉其遍历的过程,只需要每次取出一个想要的数据进行处理就可以了。但是在使用的时候也是有不同的。

List和Set都有iterator()来取得其迭代器。对List来说,也可以通过listIterator()取得其迭代器,两种迭代器在有些时候是不能通用的,Iterator和ListIterator主要区别在以下方面:

  • (1)ListIterator有add()方法,可以向List中添加对象,而Iterator不能
  • (2)ListIterator和Iterator都有hasNext()和next()方法,可以实现顺序向后遍历,但是ListIterator有hasPrevious()和previous()方法,可以实现逆向(顺序向前)遍历。Iterator就不可以。
  • (3)ListIterator可以定位当前的索引位置,nextIndex()和previousIndex()可以实现。Iterator没有此功能。
  • (4)都可实现删除对象,但是ListIterator可以实现对象的修改,set()方法可以实现。Iierator仅能遍历,不能修改。
    因为ListIterator的这些功能,可以实现对LinkedList等List数据结构的操作。其实,数组对象也可以用迭代器来实现。
Collection 和 Collections区别
  • (1)java.util.Collection 是一个集合接口(集合类的一个顶级接口)。它提供了对集合对象进行基本操作的通用接口方法。Collection接口在Java 类库中有很多具体的实现。Collection接口的意义是为各种具体的集合提供了最大化的统一操作方式,其直接继承接口有List与Set。

    Collection
    ├List
    │├LinkedList
    │├ArrayList
    │└Vector
    │ └Stack
    └Set

  • (2)java.util.Collections 是一个包装类(工具类/帮助类)。它包含有各种有关集合操作的静态多态方法。此类不能实例化,就像一个工具类,用于对集合中元素进行排序、搜索以及线程安全等各种操作,服务于Java的Collection框架。
    代码示例:

    import java.util.ArrayList; 
    import java.util.Collections; 
    import java.util.List; 
    
    public class TestCollections { 
    
        public static void main(String args[]) { 
            //注意List是实现Collection接口的 
            List list = new ArrayList(); 
            double array[] = { 112, 111, 23, 456, 231 }; 
            for (int i = 0; i < array.length; i++) { 
                list.add(new Double(array[i])); 
            } 
            Collections.sort(list); 
            for (int i = 0; i < array.length; i++) { 
                System.out.println(list.get(i)); 
            } 
            // 结果:23.0 111.0 112.0 231.0 456.0 
        } 
    }

多线程