Java核心技术 卷 I 读书笔记

Java基础

Java位运算符

与(&)、或(|)、非(~)、异或(^)

  • 与(&):两个操作数中位都为1,结果才为1,否则结果为0
  • 或(|):两个位只要有一个为1,那么结果就是1,否则就为0
  • 非(~):如果位为0,结果是1,如果位为1,结果是0
  • 异或(^):两个操作数的位中,相同则结果为0,不同则结果为1
public class Count{
  public static void main(String[] args){
	int a=129;   // a转换为二进制是10000001
	int b=128;   // b转换为二进制是10000000
	System.out.println("a 和b 与的结果是:"+(a&b));   // 128
    System.out.println("a 和b 或的结果是:"+(a|b));   // 129
    System.out.println("a 非的结果是:"+(~a));   // 0b1111110
    System.out.println("a 与 b 异或的结果是:"+(a^b));   // 1
  }
}

String类常用API

方法说明
int length()返回当前字符串长度
int indexOf(int ch)int indexOf(String str)查找字符或字符串在该字符串中第一次出现的位置
int indexOf(int ch, int fromIndex)int indexOf(String str, int fromIndex)fromIndex:开始搜索的索引位置
int lastIndexOf(int ch)intlastIndexOf(String str)查找字符或字符串在该字符串最后一次出现的位置
int lastIndexOf(int ch, int fromIndex)int lastIndexOf(String str, int fromIndex)fromIndex:开始搜索的索引位置
String substring(int beginIndex)String substring(int beginIndex, int endIndex)获取从beginIndex位置开始到结束或endIndex位置的子字符串
String trim()去除字符串里的空格
boolean equals(Object obj)将该字符串与指定对象比较
String toLowerCase() / String toUpperCase()将字符串转换为小 / 大写
char charAt(int index)获取字符串中指定位置的字符
String[] split(String regex, int limit)将字符串分割为子字符串,返回字符串数组
byte[] getBytes()将该字符串转换为byte数组

StringBuilder类常用API

StringBuilder线程不安全!!StringBuffer安全!!

Java编译器对String做了特殊处理,使得我们可以直接用+拼接字符串。

考察下面的循环代码:

String s = "";
for (int i = 0; i < 1000; i++) {
    s = s + "," + i;
}

然可以直接拼接字符串,但是,在循环中,每次循环都会创建新的字符串对象,然后扔掉旧的字符串。这样,绝大部分字符串都是临时对象,不但浪费内存,还会影响GC效率。

为了能高效拼接字符串,Java标准库提供了StringBuilder,它是一个可变对象,可以预分配缓冲区,这样,往StringBuilder中新增字符时,不会创建新的临时对象:

StringBuilder sb = new StringBuilder(1024);
for (int i = 0; i < 1000; i++) {
    sb.append(',');
    sb.append(i);
}
String s = sb.toString();
方法说明
StringBuilder()构造一个空的字符串构建器
int length()返回构建器字符串长度
StringBuilder append(String str)StringBuilder append(char c)追加一个字符串并返回this
StringBuilder appendCodePoint(int cp)追加一个代码点,并将其转换为一个或两个代码单元并返回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的字符串
String toString()返回一个构建器内容相同的字符串

Scanner类常用API

方法说明
Scanner()新建一个Scanner类对象,参数可以是字符串或是File对象
void close()关闭此扫描器
boolean hasNextXXX()判断是否还有下一个输入项,其中Xxx可以是Int,Double等,如果需要判断是否包含下一个字符串,则可以省略Xxx
boolean nextXXX()获取下一个输入项,Xxx的含义和上一个方法中的Xxx相同,默认情况下,Scanner使用空格,回车等作为分隔符
String nextLine()此扫描器执行当前行,并返回跳过的输入信息。
String toString()返回此Scanner的字符串表示形式

文件输入与输出

想要对文件进行读取,就需要一个用File对象构造一个Scanner对象:

Scanner in = new Scanner(Paths.get("my.txt"), "UTF-8");

想对文件进行写入,就需要构造一个PrintWriter对象:

PrintWriter out = new PrintWriter("my.txt", "UTF-8");  //若文件不存在则创建该文件

注:严禁用一个不存在的文件构造Scanner或用一个不能被创建的文件名构造一个PrintWriter,Java解释器认为这类异常比“零除”更严重

实现类似Python的try-except操作:

public static void main(String[], args) throw IOException
{
	Scanner in = new Scanner(Paths.get("my.txt"), "UTF-8");
	...
}

Java数组

int[] a;   // 声明a为整型数组
int[] a = new int[100];   // 创建长度为100的数组
for each循环

基本格式:for (iterable : collection) statement

collection这一集合表达式必须是一个数组或是实现了Iterable接口的类对象

for(int element : a)
    System.out.println(element);
数组初始化及匿名函数
int[] array1 = {2, 3, 5, 7, 13};   // 不需要调用new
array1 = new int[] {2, 3, 5, 7, 13};   // 两种方法作用相同

// 直接初始化一个匿名数组
for (int e : new int[] {2, 3, 5, 7, 13}) System.out.println(e);
数组拷贝
int[] array2 = array1;
array[2] = 9;   // 此时array1的结果也会改变

在Java中,允许将一个数组变量拷贝给另一个数组变量。这是两个变量将引用同一个数组。

若要有值拷贝到一个新的数组中,可以调用Arrays类的copyOf方法:

int[] array2 = Arrays.copyOf(array1, array1.length);

第二个参数是新数组的长度,多余元素将被赋值为0,如果数组为boolean类型,多余元素则为false,如果长度小于原数组长度,则只拷贝最前面的元素。

数组排序
int[] a = new int[10000];
...
Arrays.sort(a);   // 快速排序算法
常用API
方法说明
String toString(type[] a)返回包含数组元素的字符串,并用,和括号分隔
type copyOf(type[] a, int length)拷贝长度为length的数组
type copyOfRange(type[] a, int start, int end)拷贝长度为length的数组,可根据startend设置起始终止下标
void sort(type[] a)采用优化的快速排序算法对数组进行排序
int binarySearch(type[] a, int start, int end, type v)二分查找发查找值v,成功则返回对应下标值,某则返回负数-r,可根据startend设置起始终止下标
void fill(type[] a, type v)将数组所有元素值设置为v
boolean equals(type[] a, type[] b)如果两个数组大小、下标、对应元素都相同,返回true

对象与类

  • 类之间的关系
    • 依赖——“uses-a“,依赖关系例如一个类的方法操纵这另一个类对象(应尽量减少)

    • 聚合——”has-a”,聚合关系意味着例如类A对象包含于类B对象

    • 继承——”is-a“

  • 构造器
    • 构造器与类名同名
    • 每个类可以有一个以上的构造器
    • 构造器可以有0个、1个或多个参数
    • 构造器没有返回值
    • 构造器总是伴随着new操作一起调用

注:必须注意在所有的方法中不要命名与实例域同名的变量

警告:不要编写返回引用可变对象的访问器方法

class Employee
{
    private Date hireDay;
    private String name;
    ...
    public Date getHireDay()
    {
        return hireDay;   // not good
    }
    ...
}

Employee harry = ...;
Date d = harry.getHireDay();
d.setTime(d.getTime() - 60 * 60 * 1000.0);   // WRONG!!!

在上述代码中,dharry.harryDay其实引用了同一个对象!!

对d使用更改其方法就会破坏hireDay的私有状态!!!

如需返回一个可变对象的引用,应当先对其克隆(clone)

public Date getHireDay()
{
    return (Date) hireDay.clone();   // good use
}
基于类的访问权限
class Employee
{
    ...
    public boolean equals(Employee other)
    {
        return name.equals(other.name);
    }
}

// 调用上述方法
if (harry.equals(boss)) ...   // correct!

同一个类的方法可以访问该类不同对象的私有域

final修饰符大多修饰基本类型域或不可变类的域,对于可变的类,使用final可能会造成混乱

静态域与静态方法——static
class Employee
{
    public static final int nextId = 1;
    ...
}

静态类也被称为类域,对该类的所有对象而言只有唯一一个属于类的拷贝;

静态方法是一种不能向对象实施操作的方法,例如Math.sqrt()

静态方法可以访问自身类中的静态域,例如:

public static int getNextId()
{
    return nextId;   // return static field
}

只推荐在一下两种情况使用静态方法:

  • 一个方法不需要访问对象状态,其所需参数都是通过显示参数提供(例如:Math.sqrt()
  • 一个方法只需要访问类的静态域(例如:Employee.getNextId()

静态方法也可以当作工厂方法来构造对象

LocalDate t = LocalDate.now();

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

注:每一个类可以有一个main方法。这是一个常用于对类进行单元测试的技巧。

《Java核心技术 卷I》第119页 方法参数

对于一个方法的参数,一般有两种类型(1、基本数据类型,2、对象引用)

  • 基本数据类型——作为方法参数不会被修改
  • 对象引用——方法得到的是对象引用的拷贝,对象引用及其拷贝同时引用同一个对象

注:这就意味着当该对象引用的拷贝发生改变时,其引用的对象也会发生改变

Java方法参数总结
  • 一个方法不能修改一个基本数据类型的参数(即数值型或布尔型)
  • 一个方法可以改变一个对象参数的状态
  • 一个方法不能让对象参数引用一个新的对象
对象构造
  • 显式域初始化
class Employee
{
    private String name= "";   // 在执行构造器之前,先执行赋值操作
    ...
}
  • 参数名
// 命名小技巧
public Employee(String aName, double aSalary)
{
    name = aName;
    salary = aSalary;   // 在每个参数前加一个前缀a
}
  • 不同构造器之间相互调用
public Employee(double aSalary)
{
    // calls Employee(String, double)
    this("John", aSalary);
}
  • 初始化模块(initialization block)
class Employee{
    private static int nextId;
    
    private int id;
    private String name;
    private double salary;
    
    {  // Runs each time you instantiate an object
        id = nextId;
        nextId++;
    }
    ...
}
  • 静态初始化模块与运行顺序
public class Initialization {
  public static void main(String[] args) {

    InitClass t1 = new InitClass();
    InitClass t2 = new InitClass();
  }
}

class InitClass {
  static int a = 100;

  public InitClass() {
    System.out.println("我是构造器");
  }

  {
    System.out.println("我是普通初始化块");
  }

  static {
    System.out.println("我是静态初始化块");
  }
}

/*
我是静态初始化块
我是普通初始化块
我是构造器
我是普通初始化块
我是构造器
*/

结论:先执行静态初始化模块(只执行一次),再执行普通初始化模块,最后执行构造器

类设计技巧
  • 一定要保证数据私有
  • 一定要对数据初始化
  • 不要再类中使用过多的基本类型
  • 不是所有的域都需要独立的域访问器和域更改器
  • 优先使用不可变的类

继承

类、子类和超类

  • 定义子类
public class Manager extends Employee
{
	添加方法和域...
}

Manager类也就是新类成为子类派生类孩子类,已存在的类成为超类父类基类

  • 子类构造器
public Manager(String aName, double aSalary, double aBonus){
    super(aName, aSalary);
    this.bonus = aBonus;
  }
  • 覆盖超类方法
@Override
public double getSalary()
{
    return super.getSalary() + this.bonus;
}

通过特定的关键字super来调用超类的getSalary()方法,如果要覆盖父类方法,要在方法前加@Override

  • 多态

超类对象的任何地方都可以用子类对象置换

Employee e;
e = new Employee(...);
e = new Manager(...);   // 可以将一个子类对象赋给超类

Manager boss = new Manager(...);
Employee staff = new Employee[3];
// staff[0]与boss引用同一个对象,但编译器将staff[0]看成Employee对象
staff[0] = boss;
// 假设set_bonus是Manager子类的方法
boss.set_bonus(5000);   // OK
staff[0].set_bonus(5000);   // ERROR

子类数组的引用可以转换成超类数组的引用,而不需要强制类型转换

注:严禁出现以下错误!!

Manager[] managers = new Manager[10];
Employee[] staff = managers;   // 合法操作
staff[0] = new Employee("George", ...);   // 合法操作
staff[0].setBonus(10000);   // 一个不存在的实例域被调用,会搅乱相邻存储空间的内容
  • 阻止继承:final类和方法
public final class Executive extends Manager
{
	...
}

在定义类时加上final修饰符就表明这个类是final类,不允许扩展

public class Employee{
	...
    public final String getName(){
        return name;
    }
}

将方法或类声明为final主要目的是:确保它们在子类中不会改变语义

  • Java动态绑定与静态绑定

Q:当子类和父类存在同一个方法,子类重写了负类的方法,程序在运行是调用的是父类的方法还是子类的重写方法呢?

Q:当一个类中存在方法名相同但参数不同(重载)的方法,程序在执行时该如何判别区分使用哪种方法呢?

绑定

将一个方法的调用与方法所在的类关联起来。即决定调用哪个方法和变量。在Java中,绑定分为静态绑定与动态绑定。

静态绑定

在Java的方法中只有finalstaticprivate修饰的方法是静态绑定的

private修饰的方法:不能被继承,因此子类无法访问父类的private修饰的方法,只能通过父类的对象来调用。因此可以说private方法和这个类绑定在了一起

final修饰的方法:可以被子类继承,但不能被子类重写

static修饰的方法:可以被子类继承,但不能被子类重写

动态绑定

动态绑定过程1、虚拟机提取对象实际类型的方法表 2、虚拟机搜索方法签名 3、调用方法

  • 强制类型转换
Employee staff = new Employee[10];
Manager boss = (Manager) staff[0];

将一个超类的引用赋给一个子类变量,必须进行类型转换

可以用instanceof操作符实现对类型转换异常的检测

if (staff[1] instanceof Manager)
{
    boss = (Manager) staff[1];
    ...
}

如果这个类型转换不可能成功,那么编译器会报出编译错误,例子如下

String c = (String) staff[1];   // Error

综上所述:

  • 强制类型转换只能在继承层次内进行类型转换
  • 在将超类转换为子类之前,应该用instanceof进行检查
抽象类与抽象方法

抽象类更多的体现的是一个模板的作用,以该抽象类作为子类的模板可以避免子类设计的随意性

  • 抽象方法和抽象类的规则
    • 抽象方法和抽象类必须由abstract修饰符修饰,同时,修饰方法不能有实际方法体

    • 抽象类不能实例化,不能由new来调用抽象类的构造器来创建实体

    • 含有抽象方法的类只能被定义为抽象类

抽象类

abstract class Staff {
  private double salary;
  private String name;

  {
    System.out.println("Staff初始化模块被调用");
  }

  public Staff() {
  }

  public Staff(String aName, double aSalary) {
    this.salary = aSalary;
    this.name = aName;
    System.out.println("执行Staff的构造器");
  }

  public void setName(String aName) {
    this.name = aName;
  }

  public void setSalary(double aSalary) {
    this.salary = aSalary;
  }

  public String getName() {
    return this.name;
  }

  public double getSalary() {
    return this.salary;
  }

  public abstract String getType();

  public abstract void showInfo();
}

子类

class Supervisor extends Staff {
  private double bonus = 0;
  private double performance = 1;

  {
    System.out.println("Supervisor初始化模块被调用");
  }

  public Supervisor(String aName, double aSalary, double aBonus, double aPerformance) {
    super(aName, aSalary);
    this.bonus = aBonus * aPerformance;
    this.performance = aPerformance;
    System.out.println("执行Supervisor的构造器1");
  }

  public Supervisor(String aName, double aSalary) {
    super(aName, aSalary);
    System.out.println("执行Supervisor的构造器2");
  }

  public void setBonus(double aBonus) {
    this.bonus = aBonus * this.performance;
  }

  public void setPerformance(double aPerformance) {
    this.performance = aPerformance;
    this.bonus *= aPerformance;
  }

  @Override
  public String getType() {
    return "Supervisor";
  }

  @Override
  public void showInfo() {
    System.out.println("员工姓名:" + super.getName());
    System.out.println("员工岗位:" + this.getType());
    System.out.println("员工基本工资:" + super.getSalary());
    System.out.println("员工绩效提成百分比:" + (this.performance - 1) * 100 + "%");
    System.out.println("员工提成:" + super.getSalary() * this.performance);
  }
}
受保护访问

java中用于控制可见性的4个访问修饰符

  • private——仅对本类可见
  • public——对所有类可见
  • protected——对本包和所有子类可见
  • default(默认,不需要写明)——对本包可见
Object:所有类的超类

Object类是Java所有类的始祖,在Java中每个类都是由它扩展而来

  • equals方法

    Object类中的euqals方法用于检测一个对象是否等于另外一个对象

泛型数组列表

在Java中,解决运行时动态更改数组问题最简单的方法是使用**ArrayList类,它是一个采用类型参数泛型类**。

声明和构造一个保存Employee对象的数组列表:

ArrayList<Emoloyee> staff = new ArrayList<>();   // Java SE7

使用add方法将元素添加到数组列表中:

staff.add(new Employee("George", 13000, ...));

如果已经清楚或能估计数组大小,可以在填充数组之前调用ensureCapacity方法:

staff.ensureCapacity(100);

这个方法将分配一个包含100个对象的内部数组。下100次调用add将不用重新分配空间

ArrayList常用API
方法说明
ArrayList<E>()构造一个空数组列表
ArrayList<E>(int initialCapacity)用指定容量构造一个空数组列表
boolean add(E obj)在数组列表末尾添加一个元素。永远返回**true**
boolean add(int index, E obj)在某个位置插入新元素,位于index之后的所有元素都要向后移一个位置
int size()返回存储在数组列表中的当前元素数量
void ensureCapacity(int capacity)分配存储空间
void trimToSize()将数组列表的存储容量削减到当前尺寸
访问数组列表元素

ArrayList类使用setget方法实现改变或访问数组元素的操作。

方法说明
void set(int index, E obj)修改某个位置的元素
E get(int index)获取某个位置的元素
E remove(int index)移除某个元素

泛型数组的兼容性要注意!

对象包装器与自动装箱

Integer类对应基本类型int,所有的基本类型都有一个与之对应的类,这些类被成为包装器

这些对象包装类分别是:Integer、Long、Float、Double、Short、Byte、Character、Void和Boolean。

ArrayList<Integer> n_list = new ArrayList<>();

注:由于每个值都被包装在对象中,所有ArrayList<Integer>的效率远远低于int[] 数组。

  • 自动装箱与自动拆箱

    如果这时调用

    n_list.add(3);
    

    编译器会自动把这条语句变成

    n_list.add(Integer.valueOf(3));
    

两个对象包装器的比较需要调用**equals**方法

参数可变的方法
public static double max(double... values) {
    double largest = Double.NEGATIVE_INFINITY;
    for (double v : values) if (v > largest) largest = v;
    return largest;
}

调用

double m = max(3.1, 30)

编译器会将new double[] {3.1, 30}传递给max方法

枚举类
enum Size {
  SMALL("S"), MEDIUM("M"), LARGE("L"), EXTRA_LARGE("XL");

  private String size;

  private Size(String size) {
    this.size = size;
  }

  public String getSize() {
    return this.size;
  }
}
常用API
方法说明
static Enum valueOf(Class enumClass, String name)返回指定名字、给定类的枚举常量
static toString()返回枚举常量名
int ordinal()返回枚举常量在enum声明中的位置
int compareTo(E other)如果枚举常量出现在other之前,返回一个负值,如果this==other,返回0.否则返回正值

反射

能够分析类能力的程序被称为反射。反射机制功能极为强大,其可以用来:

  • 在运行时分析类的能力
  • 在运行时查看对象。例如,编写一个toString()方法供所有类使用
  • 实现通用的数组操作代码
  • 利用Method对象,这个对象很像C++中的函数指针
反射机制相关类

反射属于Java的高级特性,JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;这种动态获取信息以及动态调用对象方法的功能称为java语言的反射机制。

类名用途
Class类代表类的实体,在运行的Java应用程序中表示类和接口
Field类代表类的成员变量(成员变量也称为类的属性)
Method类代表类的方法
Constructor类代表类的构造方法

例子:

Random generator = new Random();
Class c1 = generator.getClass();
String name = c1.getName();   // name = "java.util.Random"

反射可以通过一个对象、成员变量、属性等来获得整个类,非常方便,这其中涉及类加载器等balabala。。。慢慢来

反射的知识很深奥!!需要慢慢理解!!只写了一小部分!!

接口

接口概念

官方解释:Java接口是一系列方法的声明,是一些方法特征的集合,一个接口只有方法的特征没有方法的实现,因此这些方法可以在不同的地方被不同的类实现,而这些实现可以具有不同的功能。

个人理解:接口可以理解为一种特殊的类,里面全部是有全局变量公共抽象方法组成。接口是解决Java无法多继承的一种方法,但接口在实际中更多的是用来制定标准,或者理解为100%的抽象类,即接口中的方法全部都是抽象方法。

接口的语法实现

为了声明一个接口,要使用interface这个关键字,在接口中所有方法都必须只声明方法标识,而不要声明具体方法体。接口中的属性默认为public static final。一个类要实现这个接口必须实现这个接口中定义的所有抽象方法

public class TestInterface {
  public static void main(String[] args) {
    testClass t = new testClass();
    t.show();
    System.out.println(testClass.a);
  }
}

interface in1 {
  int a = 10;

  void show();
}

class testClass implements in1{
  public void show() {
    System.out.println("in1.show()" + a);
  }
}
接口与抽象类
  • 接口也可以被扩展
  • 每个类只能有一个超类,但却可以实现多个接口
  • 接口不能实例化,但却可以声明接口变量
接口方法冲突
  • 超类优先

    如果超类与默认方法提供了一个具体方法,同名而且有相同参数类型的默认方法会被忽略。

  • 接口冲突
    class Student implents Person, Named {
        public String getName() { return Person.super.getName(); }
        ...
    }
    

    如果两个接口其中有一个冲突的方法不是默认方法,编译器则会报错要求解决二义性。

接口与回调
public class CallBackTest {
  public static void main(String[] args) {
    TaxiDriver td = new TaxiDriver();
    Passenger passenger = new Passenger();
    passenger.TakeTaxi(td);
  }
}

interface Callback {    // 接口
  boolean Consider(int money);

  void PayFor(int money);

}

class TaxiDriver {
  public int Answer(Callback callback) {
    System.out.println("去那的话要100块");
    if (callback.Consider(100) == true) {
      callback.PayFor(100);   // 回调
    }
    return 100;
  }
}

class Passenger implements Callback {
  @Override
  public boolean Consider(int money) {
    boolean result = true;
    if (money > 80) {
      System.out.println(money + "太贵了,您看80行不行?");
      result = false;
    }
    return result;
  }

  @Override
  public void PayFor(int money) { System.out.println("好的,这是您的" + money); }

  public void TakeTaxi(TaxiDriver td) {
    System.out.println("师傅,去天一多少钱?");
    td.Answer(this);
  }
}

回调是一种常见的程序设计模式,利用回调技术可以处理这样的问题,事件A发生时要执行处理事件A的代码,判断何时发生事件A及何时执行处理事件A的代码。这些代码是固定的,先行编写完毕,供使用。但事件A的处理代码开放给其他开发人员编写,可以有很多不同的实现,使用时可以注册具体需要的实现来处理。

对象克隆
Employee origin = new Employee("John", 13000);
Employee copy = origin;   // 获取对象的引用
copy.raiseSalary(1000);   // 此时origin对象发生改变

// 如果希望copy是一个新对象,且初始状态与origin相同,可以使用clone方法
Employee copy = origin.clone();
copy.raiseSalary(1000); 

// 默认浅拷贝
@Override
protected Object clone() throws CloneNotSupportedException {
  return super.clone();
}

// 深拷贝
protected object clone() throws CloneNotSupportedException {
    Employee cloned = (Employee) super.clone();
    cloned.hireDay = (Date) hireDay.clone();
    return cloned;
}

默认的克隆操作是“浅拷贝”,并没有克隆对象中引用的其他对象。如果引用的其他对象属于不可变的类,例如String,那么这种浅克隆的共享子对象就是安全的。而通常子对象都是可变的,必须重新定义clone方法来建立一个深拷贝,同时克隆出所有的子对象。例如Employee类中的hireDay域是一个Date,所以它也需要克隆。

lambda表达式

lambda表达式语法格式:

(parameters) -> expression
// 或
(parameters) -> { statements; }
函数式接口

对于只有一个抽象方法的接口,需要这种接口的对象时,就可以提供一个lambda表达式。这种接口被称为函数式接口

例如:

// Arrays.sort第二个参数需要一个Comparator实例,Comparator就是只有一个方法的接口。
String[] planets = new String[] { "Mercury", "Venus", "Earth", "Mars", "Jupiter"};

Arrays.sort(planets, (first, second) -> first.length() - second.length());

在底层,Arrays.sort方法接收 实现了Comparator的某个类的对象。在这个对象上调用compare方法会执行这个lambda表达式的体。这些对象和类的管理完全取决于具体实现,且比传统的内联类高效得多。

注:最好把lambda表达式看作是一个函数,而不是一个对象

Timer t = new Timer(1000, event -> {
    System.out.println("the time is " + new Date());
})

Java API在java.util.function包中定义了很多非常通用的函数式接口

方法引用

在学习lambda表达式之后,我们通常使用lambda表达式来创建匿名方法。然而,有时候我们仅仅是调用了一个已存在的方法。

什么是方法引用?

方法引用是用来直接访问类或者实例的已经存在的方法或者构造方法。方法引用提供了一种引用而不执行方法的方式,它需要由兼容的函数式接口构成的目标类型上下文。计算时,方法引用会创建函数式接口的一个实例。

四种方法引用类型

类型方法语法对应的Lambda表达式
静态方法引用类名::staticMethod(args) -> 类名.staticMethod(args)
实例方法引用类名::instanceMethod(args) -> 类名.instMethod(args)
对象方法引用类名::instMethod()
构造方法引用类名::new

异常

如果由于出现错误而是的某些操作没有完成,程序应该:
  • 返回到一种安全状态,并能够让用户执行一些其他的命令
  • 允许用户保存所有操作的结果,并以妥善的方式终止程序

异常分类

异常对象都是派生于**Throwable**类的一个实例

Throwable
Error
Exception
IOException
RuntimeException

**Error**类层次描述了 Java 运行时系统的内部错误和资源耗尽错误。

在设计 Java 程序时,需关注**Exception层次结构。这种层次结构又分解位两个分支:一个分支派生于RuntimeException**; 另一个分支包含其他异常。划分的规则是:由程序错误导致的异常属于RuntimeException;而程序本身没有问题,但类似像 I/O 错误这类问题导致的异常则属于其他异常。

注:如果出现RuntimeException异常,那么就一定是你的问题
派生于RuntimeException的异常包含以下几种情况:
  • 错误的类型转换
  • 数组访问越界
  • 访问 null 指针

Java 语言规范将派生于 Error 类或 RuntimeException 类的所有异常成为非受查(unchecked)异常,所有其他异常称为受查(checked)异常

遇到下面四种情况时应该抛出异常:
  • 调用一个抛出受查异常的方法,例如 FileInputStream 构造器
  • 程序运行过程中发现错误,并利用 throws 语句抛出一个受查异常
  • 程序出现错误
  • Java 询价和运行时库出现的内部错误
注:子类方法可以抛出比父类更特定的异常或根比不抛出,但不能抛出父类方法没有 throws 的异常
捕获多个异常
try {
    code that might throw exceptions
}
catch (FileNotFountException | UnknownHostException e) {
    pass
}
catch (IOException e) {
    pass
   	e.getMessage();   // 得到更详细的信息
    e.getClass().getName()   // 得到异常对象的实际类型
}
再次抛出异常与异常链

捕获异常并将它再次抛出的基本方法。

try {
	access the database
}
catch (SQLException e) {
    throw new ServletException("database error: " + e.getMessage());
}

这里,ServletException 用带有异常信息文本的构造器来构造

不过,有一种更好的处理方法,并且将原始异常设置为新异常的“原因”

try {
    access the database
}
catch (SQLException e) {
    Throwable se = new Servlet("database error");
    se.initCause(e);   // initCause()方法初始化该throwable为指定值的原因
    throw se;
}

当捕获到异常时,就可以用下面这条语句重新获得原始异常:

Throwabel e = se.getCause();

强烈建议使用这种包装技术。这样可以让用户抛出子系统中的高级异常,而不会丢失原始异常的细节。

finally 子句

finally 子句并不是必须存在的,不过开发过程中建议加上finally,里面可以使用来执行打印日子代码,给出现问题时查看日子买下伏笔,做一些善后清理工作。

有意思的是,即使try里包含continue,break,return,try块结束后,finally块也会执行。

如果 try 子句和 finally 子句中都有 return 返回值,那么同时执行时 finally 会覆盖原始返回值

带资源的try语句(try-with-resources)

带资源的try语句,它允许在try关键字后紧跟一对圆括号,圆括号可以声明、初始化一个或多个资源(此处的资源是指那些必须在程序结束时显式关闭的资源,比如数据库连接,网络连接等)

try-with-resources 是一个定义了一个或多个资源的 try 声明,try语句在该语句结束时自动关闭这些资源。try-with-resources确保每一个资源在处理完成后都会被关闭。这些资源必须实现 AutoCloseable 或者 Closeable 接口,实现这两个接口就必须实现close() 方法。

try (Scanner in = new Scanner(new FileInputStream("d:\\haha.txt"));
     PrintWriter out = new PrintWriter("d:\\hehe.txt"))
{
    while (in.hasNext()) {
        out.println(in.next().toUpperCase());
    }
} catch (FileNotFoundException e) {
    e.printStackTrace();
}
堆栈轨迹(stack trace)

堆栈轨迹是一个方法调用过程的列表,它包含了程序执行过程中方法调用的特定位置。

可以调用 Throwable 类的 printStackTrace 方法访问堆栈轨迹的文本描述信息,或者调用getStackTrace 方法得到一个 StackTraceElement 对象数组,你可以在程序中分析这个数组

Throwable t = new Throwable();
StackTraceElement[] frames = t.getStackTrace();
for(StackTraceElement frame : frames)
    System.out.println(frame);
/*
StackTraceTest.factorial(StackTraceTest.java:6)
StackTraceTest.main(StackTraceTest.java:24)
*/
/*
阶乘递归打印函数的堆栈情况
*/
import java.util.Scanner;

public class StackTraceTest {
  public static int factorial(int n) {
    System.out.println("factorial(" + n + "):");
    Throwable t = new Throwable();

    StackTraceElement[] frames = t.getStackTrace();
    t.printStackTrace();
    for (StackTraceElement f : frames)
      System.out.println(f);

    int r;
    if (n <= 1) r = 1;
    else r = n * factorial(n - 1);
    System.out.println("return " + r);
    return r;
  }

  public static void main(String[] args) {
    Scanner in = new Scanner(System.in);
    System.out.println("Enter n: ");
    int n = in.nextInt();
    factorial(n);
  }
}
java.lang.Throwable
方法名作用
Throwable(Throwable cause)
Throwable(String message, Throwable cause)用给定"原因"构造一个Throwable对象
Throwable getCause()获得设置为这个对象的"原因"的异常对象
StackTraceElement[] getStackTrace()获得构造这个对象时调用堆栈的跟踪
  • 捕获异常机制所消耗的时间远大于不进行捕获,所以只在异常情况下使用异常机制
  • 不要过分地细化异常
  • 利用好异常层次结构
  • 不要压制异常
  • 在检测错误时,"苛刻"比放任更好
  • 不要羞于传递异常

使用断言

Java 断言的两种形式:
assert 条件;
assert 条件 : 表达式;

这两种形式都会对条件进行检测,如果为 false ,则抛出一个 AssertionError 异常。在第二种形式中,表达式被传入 AssertionError 的构造器,并转换成一个消息字符串

注:“表达式” 部分的唯一目的是产生一个消息字符串。

启用和禁用断言

在默认情况下,断言是被禁用的。可以在命令行运行时用 -enableassertions-ea 选项启用:

java -enableassertions Test.java

在启用或禁用断言时不必重新编译程序。

禁用断言,可以在命令行运行时用 -disableassertions-da 选项启用:

上述断言命令选项不能用于那些没有类加载器的"系统类"中。

Java 语言中,给出了3种处理系统错误的机制:
  • 抛出一个异常
  • 日志
  • 使用断言
什么时候该使用断言呢?应记住以下几点:
  • 断言失败是致命的、不可恢复的错误
  • 断言检查只用于开发和测试阶段

在调用API时,可以阅读文档,确定可能出现的错误,例如如下的排序方法

/*
 * @param a the array to be sorted
 * @param fromIndex the index of the first element, inclusive, to be sorted
 * @param toIndex the index of the last element, exclusive, to be sorted
 *
 * @throws IllegalArgumentException if {@code fromIndex > toIndex}
 * @throws ArrayIndexOutOfBoundsException
 *     if {@code fromIndex < 0} or {@code toIndex > a.length}
 */
public static void sort(int[] a, int fromIndex, int toIndex) 

文档指出了方法可能出现的错误,比如出现错误的索引值,那么就会抛出一个异常。这是方法与调用者之间约定的处理行为。如果实现这个方法,就应该遵守约定,并抛出索引值有误的异常,因此不适合用断言。

如果:

/*
* @param a the array to be sorted
* 变为
* @param a the array to be sorted (must not be null)
*/

此时不允许用null数组去调用这个方法,就应该在调用方法前使用断言

assert array != null;

这种约定被称为前置条件(Precondition)

泛型程序设计

泛型程序设计(Generic programming) 意味着编写的代码可以被很多不同类型的对象所重用。例如 ArrayList 类可以聚集任何类型的对象。

在 Java 增加泛型类之前,泛型程序设计是用继承实现的(CoreJava I 309页)。ArrayList类只维护一个 Object 引用的数组:

public class ArrayList  // 产生泛型类之前
{
 private Object[] elementData;
 ...
 public Object get(int i) {...}
 public Object add(Object o) {...}
}

这种方法有两个问题:

  • 当获取一个值时必须进行强制类型转换
  • 没有错误检查,可以向数组列表中添加任何类的对象。

泛型类提供了一个类型参数(type parameters) 用来指示元素的类型,例如:

ArrayList<String> files = new ArrayList<String>();
// 在 Java SE 7之后,构造函数中可以省略泛型类型

类型参数的魅力在于:使得程序具有更好的可读性安全性

定义简单的泛型类

一个泛型类就是具有一个或多个类型变量类型的类。

注:类型变量尽量使用大写,且比较短,E表示集合的元素类型。K和V分别表示表的关键字与值的类型。T表示 “任意类型”。

类型变量的限定

如何确保类型 T 变量所属的类有 compareTo 方法呢?解决这个问题的方案是将 T 限制为实现了 Comparable 接口的类。

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

<T extends BoundingType> 表示 T 绑定的子类型。绑定类型既可以是类,也可以是接口。

限定类型用 “&” 分隔,而 逗号 用于分隔类型变量。

一个简单的泛型类例子如下:
package pair;

import java.time.LocalDate;

public class PairTest1 {
  public static void main(String[] args) {
    String[] words = {"Mary", "had", "a", "little", "lamb"};
    Pair<String> mm = ArrayAlg.minmax(words);
    System.out.println("min = " + mm.getFirst());
    System.out.println("max = " + mm.getSecond());
    System.out.println("test_min = " + ArrayAlg.<String>min(words));

    System.out.println("-------------------------------------------------------");

    LocalDate[] birthday =
            {
                    LocalDate.of(2000, 12, 25),
                    LocalDate.of(2000, 8, 28),
                    LocalDate.of(2000, 9, 30),
                    LocalDate.of(2000, 4, 28),
                    LocalDate.of(2018, 1, 13),
            };
    Pair<LocalDate> bd = ArrayAlg.minmax(birthday);
    System.out.println("min = " + bd.getFirst());
    System.out.println("max = " + bd.getSecond());
  }
}

class Pair<T> {
  private T first;
  private T 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 first) {this.first = first;}

  public void setSecond(T second) {    this.second = second;  }
}

class ArrayAlg {
	
  public static <T extends Comparable<? super T>> Pair<T> minmax(T[] a) {
    if (a == null || a.length == 0) return null;
    T min = a[0];
    T max = a[0];
    for (int i = 1; i < a.length; i++) {
      if (min.compareTo(a[i]) > 0) min = a[i];
      if (max.compareTo(a[i]) < 0) max = a[i];
    }
    return new Pair<>(min, max);
  }

  public static <T extends Comparable> T min(T[] a) {
    if (a.length == 0) return null;
    T smallest = a[0];
    for (int i = 1; i < a.length; i++) {
      if (smallest.compareTo(a[i]) > 0) smallest = a[i];
    }
    return smallest;
  }
}

泛型代码和虚拟机VM

虚拟机没有泛型类型对象——所有对象都属于普通类。

无论何时定义了一个泛型类型,都自动提供了一个相应的原始类型(raw type)

类型擦除

Java泛型的处理在编译器中运行,编译生成的字节码bytecode不包含泛型信息,泛型信息在编译处理时被擦除(erasure),这个过程即类型擦除

例如上述 Pair<T>的原始类型如下所示:

public class Pair {
    private Object first;
    private Object second;
    
    public Pair() {first = null; second = null;}
    public Pair(Object first, Object second) {
        this.first = first;
        this.second = second;
    }
    public Object getFirst() {    return first;  }

    public Object getSecond() {return second; }

    public void setFirst(Object first) {this.first = first;}

    public void setSecond(Object second) {this.second = second;  }
}

如果泛型类有类型限定,原始类型用第一个限定的类型变量来替换,如果没有限定则用 Object 来替换。

例如:

class Interval<T extends Comparable & Serializable> implements Serializable {
  private T lower;
  private T upper;
  ...
  public Interval(T first, T second) {
    if (first.compareTo(second) <= 0) {lower = first; upper = second;}
    else {lower = second; upper = first;}
  }
}

原始类型如下所示:

class Interval implements Serializable {
    private Comparable first;
    private Comparable second;
    ...
    public Interval(Comparable first, Comparable second) {...}
}

如果要用到原始类型 Serializable ,在编译器中会在必要时向 Comparable 插入强制类型转换。

因为 Comparable 包含实现方法 CompareTo ,而 Serializable 只是个标签(Tagging, 即没有方法的接口)接口,若二者对调位置,则原始类型将用Serializable来替换,这样在编译器必要时要向 Comparable 插入强制类型转换,所以为了提高效率,最好将标签(Tagging)接口放在边界列表的末尾。

JVM的指令:擦除方法返回类型,编译器将方法调用处理为两条JVM虚拟机指令

  • 对原始方法的调用(返回Object类型或限定类型)

  • 将返回的Object类型强制转换为泛型类型的具体类型(如Employee)

  • 补充#当存取一个泛型域时也要插入强制类型转换

桥方法

我们知道 Java 中的泛型在编译为 class 的时候会把泛型擦除,也就是说你写的 <T> 到最后 class 文件中其实都是 Object。

而对于泛型类的子类,例子如下:

class DataInterval extends Pair<LocalDate> {
  @Override
  public void setSecond(LocalDate second) {
    if (second.compareTo(getFirst()) >= 0)
      super.setSecond(second);
  }
  ...
}

经过编译类擦除后,变成:

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

此时在编译器中会生成一个桥方法(bridge method)

public void setSecond(Object second) {...}

来解决对setSecond 的调用多态性要求。

在 JVM 中,用参数类型和返回类型来确定一个方法。

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

泛型的约束与局限性

《CoreJava I》 P321

不能用基本类型实例化类型参数

不能用类型参数代替基本类型。因此没有 Pair<int> ,只有 Pair<Integer> 。其原因是类型擦除,擦除之后,Pair类含有 Object 类型的域,而 Object 不能存储 double 值。

类型查询只适用于原始类型

虚拟机中的对象总有一个特定的非泛型类型。因此,素有的类型查询只产生原始类型。

例如:

if (a instanceof Pair<String>)   // Error

实际上任意一个类型的 Pair 都是如此亦或是 强制类型转换:

if (a instanceof Pair<T>)   // Error
Pair<String> p = (Pair<String>) a;   // Warning--can only test that a is a Pair

同样的道理, getClass() 方法返回的也总是原始类型

Pair<String> a = new Pair<>("aaa", "bbb");
System.out.println(a.getClass());   // class pair.Pair
不能创建参数化类型的数组

不能实例化参数化类型的数组,例如:

Pair<String>[] table = new Pair<String>[10];   // Error

但是却可以参数化数组本身的类型

Pair<String>[] arr;   // Right

Varargs警告
不能实例化类型一个变量或数组
public Pair() {first = new T();  second = new T();}   // Error
不能构造泛型数组
泛型类的静态上下文中类型变量无效
不能抛出或捕获泛型类的实例
可以消除对受查异常的检查

泛型类型的继承规则

《CoreJava I》 P328

Manage[] m = ...;
Pair<Employee> result = ArrayAlg.minmax(m);   // Error

minmax 方法返回 Pair<Manager> ,而不是 Pair<Employee> ,并且这样的赋值是不合法的。

通常,Pair<T>Pair<S> 没有什么联系

通配符类型

通配符类型

Java 集合

所有的集合类都位于 java.util 包下

Java 数组不是面向对象的,存在明显的缺陷,集合弥补了数组的缺点
  • 集合类不能存放基本数据类型
  • 数组容量固定无法动态改变
  • 数组无法判断实际的元素数量

一篇还不错的文章

Collection 接口

在 Java 类库中,集合类的基本接口是 Collection 接口。这个接口有两个基本方法

public interface Collection<E>
{
	boolean add(E element);
	Iterator<E> iterator();
	...
}

add 方法用于向集合中添加元素。如果添加元素确实改变了集合就返回 true,没有发生变化则返回 false。

iterator 方法用于返回一个实现 Iterator接口的对象。可以使用这个迭代器对象一次访问集合中的元素。

Iterator 接口——迭代器

Iterator 接口包含4个方法:

public interface Iterator<E>
{
	E next();
	boolean hasNext();
	void remove();
	default void forEachRemaining(Consumer<? super E> action);
}

通过反复调用 next 方法,可以逐个访问集合中的每个元素。但是如果达到了集合的末尾,next 方法将抛出一个 NoSuchElementException 。因此,需要在调用 next 之前调用 hasNext 方法。

例如:

Collection<String> c = ...;
Iterator<String> iter = c.iterator();
while (iter.hasNext()) {
    String element = iter.next();
    ...
}
// 或用 for each 循环
for (String element : c) {
    ...
}

“for each” 循环可以与任意实现了 Iterator 接口的对象一起工作,这个接口只包含一个抽象方法:

public interface Iterable<E>
{
	Iterator<E> iterator();
	...
}

Collection 接口扩展了 Iterable 接口。因此,对于标准类库中的任意集合都可以使用 "for each"循环。

在 Java SE 8 中,甚至不用写循环。可以调用 forEachRemaining 方法并提供一个 lambda 表达式(它会处理一个元素)。将对迭代器的每一个元素调用这个 lambda 表达式,知道没有元素为止。

例如:

Iterator<String> iter = c.iterator();
iter.forEachRemaining(element -> {...})

元素被访问的顺序取决于集合类型。如果对 ArrayList 进行迭代,迭代器将从索引 0 开始顺序迭代。如果访问 HashSet 中的元素,每个元素将会按照某种随机的次序出现。

Java 迭代器的查找操作与位置变更是紧密相连的。查找一个元素的唯一方法是调用 next 方法,而在查找操作的同时,迭代器的位置随之向前移动。

因此,应将 Java 迭代器认为是位与两个元素之间。当调用 next 时,迭代器就越过一个元素,并返回刚刚越过的那个元素的引用。

Iterator 接口的 remove 方法将会删除上次调用 next 方法时返回的元素。在大多数情况下,在决定删除某个元素之前应该看一下这个元素是很具有实际意义的。然而,如果想要删除指定位置上的元素,仍需要越过这个元素。

例如删除集合中第一个元素的方法:

Iterator<String> iter = c.iterator();
iter.next();   // skip over the first element
iter.remove();   // now remove it

对 next 方法和 remove 方法的调用具有相互依赖性,如果调用 remove 之前没有调用 next 将是不合法的。如果这样做会抛出一个 IllegalStateException 异常。

泛型实用方法

由于 Collection 与 Iterator 都是泛型接口,可以编写操作任何集合类型的实用方法。例如,一个检查任意类型集合是否包含指定元素的泛型方法:

public static <E> boolean contains(Collection<E> c, Object obj)
{
    for (E element: c)
        if (element.equals(obj))
            return true;
    return false;
}

Collection 接口声明了很多很有用的方法,所有的实现类都必须提供这些方法。但实现 Collection 接口的每一个方法很繁琐。为了让是实现者更容易地实现这个接口,Java 类库提供了一个 AbstractCollection 类,它将基础方法 sizeiterator 抽象化了,但其他方法提供了例行方法,例如:

public abstract class AbstractCollection<E> implements Collection<E>
{
	...
	public abstract Iterator<E> iterator();
	
	public boolean contains(Object obj)
	{
		for (E element : this)
			if (element.equals(obj))
				return true;
		return false
	}
	...
}

在 Java SE 8 中新增了一个 default boolean removeIf(Predicate<? super E> filter) 方法

可以用 lambda 表达式删除满足条件的此集合的所有元素。

集合框架中的接口
NavigableSet
SortedSet
Set
Deque
Queue
List
Collection
Iterable
NavihableMap
SortedMap
ListIterator
Iterator
Map
RandomAccess

集合类有两个基本接口: Collection 和 Map。

Map 由于映射包含键 / 值对,所以要用 put 方法来插入:V put(K key, V value)

List 是一个有序集合。元素会增加到容器中的特定位置。可以采用两种方式访问元素:使用迭代器访问元素 或 使用一个整数索引来访问。

ListIterator 接口是 Iterator 的一个子接口。它定义了一个方法用于在迭代器位置前增加一个元素:void add(E element)

Set 接口等同于 Collection 接口,不过其方法的定义更严谨。Set 的 add 方法不允许增加重复的元素。要适当地定义 Set 的equals 方法:只要求包含相同的元素,不要求次序一样。

Java 库中的具体集合

集合类型描 述
ArrayList一种可以动态增长和缩减的索引序列
LinkedList一种可以在任何位置进行高效地插入和删除操作的有序序列
ArrayDeque一种用循环数组实现的双端队列
HashSet一种没有重复元素的无序集合
TreeSet一种有序集
EnumSet一种包含枚举类型值的集
LinkedHashSet一种可以记住元素插入次序的集
PriorityQueue一种允许高效删除最小元素的集合
HashMap一种存储键 / 值关联的数据结构
TreeMap一种键值有序排列的映射表
EnumMap一种键值属于枚举类型的映射表
LinkedHashMap一种可以记住键 / 值对添加次序的映射表
WeakHashMap一种在其值无用武之地后可以被垃圾回收器回收的映射表
IdentityHashMap一种用 == 而不是用 equals 比较键值的映射表
链表

在 Java 中,所有链表实际上都是**双向链接(doubly linked)**的。

例如 LinkedList 中的添加删除元素:

List<String> staff = new LinkedList<>();
staff.add("Amy");
staff.add("Bob");
Iterator iter = staff.iterator();
String first = (String) iter.next();   // visit first element
String second = (String) iter.next();
iter.remove();   // 删除第二个元素

链表与泛型集合之间有一个重要的区别。链表是一个有序集合,每个对象的位置十分重要。

在 Iterator 接口中没有 add 方法。相反地,在集合类库中提供了子接口 ListIterator ,其中包含了 add 方法,与 Collection.add 不同,这个方法为 void,它假定添加操作总会改变链表。

另外,ListIterator 接口有两个方法用来反向遍历列表:

ListIterator<String> iter = staff.listIterator();
String first = iter.next();   // visit first element
String second = iter.next();
iter.remove();   // 删除第二个元素
System.out.println(iter.previous());
System.out.println(iter.previousIndex());

注⚠️:调用 next 后 remove 会删除左侧元素,如果调用 previous 后 remove 则会删除右侧元素。

如果在迭代器修改集合时,另一个迭代器对其进行遍历,一定会出现混乱的状态!!

List<String> list = ...;
ListIterator<String> iter1 = list.listIterator();
ListIterator<String> iter2 = list.listIterator();
iter1.next();
iter1.remove();
iter2.next();   // throws ConcurrentModificationException

为了避免发生修改的异常,请遵循以下简单规则⚠️:可以根据需求给容器附加许多迭代器,但这些迭代器只能读取列表。另外,再单独附加一个既能读又能写的迭代器。

链表在访问特定索引值元素时必须从头开始,故在程序需要采用整数索引访问元素时,不选用链表。使用链表的唯一理由是尽可能减少在列表中插入或删除元素所付出的代价

List ListIterator LinkedList API 《CoreJava I》P362

数组列表(ArrayList)

ArrayList 详解

散列集

散列表的每一个对象都有一个***散列码(hashCode)***,独一无二,其可以用于快速地查找所需要的对象。散列码的计算方式 hashCode() 参见 P170

注:自己实现的 hashCode 方法应该与 equals 方法兼容,即如果 a.equals(b) 为 true,那么 a 与 b 必须具有相同的 hashCode。

在 Java 中,散列表用链表数组实现。每个列表被称为桶(bucket)。要想查找表中对象的位置,就要先计算它的散列码,然后与桶总数取余,所得到的结果就是保存这个元素的桶的索引值。

桶也有被占满的情况,这种现象被称为***散列冲突(hashCollision)***。这个需要用新对象与桶中的所有对象进行比较,查看这个对象是否已经存在。

要想更多地控制散列表的性能,就要指定一个初始的桶数。如果要插入到散列表中的元素太多,就会增加冲突的可能性,降低运行性能。

通常将桶数设置为预计元素个数的 75%~150%。(最好设为素数 (猜测))标准类库使用的桶数是 2 的幂数,默认值为 16。

如果散列表太慢,就需要***再散列(rehashed)***。rehashed 需要创建一个新的表,将所有元素插入到新表,然后丢弃旧表。***装填因子(load factor)***将决定何时对散列表进行 rehashed ,比如 0.75 代表表中 75% 的位置已经填入元素时,这个表就会用双倍的桶数自动进行 rehashed。

警告⚠️:再更改集中元素时要格外小心,如果元素的散列码发生了改变,元素在数据结构中的位置也会发生变化。

树集

TreeSet 类与散列集十分相似,不过它比散列集有所改进,树集是一个有序集合。可以以任意顺序将元素插入到集合中。在对集合进行遍历时,每个值将自动地按照排序后的顺序呈现。例如:

TreeSet<String> sorter = new TreeSet<>();
sorter.add("Bob");
sorter.add("Amy");
sorter.add("Candy");
for (String name : sorter) System.out.println(name);
// Amy Bob Candy

排序是使用红黑树结构完成的。将一个元素添加到树种要比添加到散列集中慢,如果树中包含 n 个元素,查找新元素的正确位置平均需要 log2n 次比较。

注:要使用树集,其元素类型必须实现 Comparable 接口,或者构造集时必须提供一个Comparator

队列与双端队列

队列可以让人们有效地在尾部添加一个元素,在头不删除一个元素(先进先出)。有两个端头的队列,即***双端队列***,可以有效地在头部和尾部同时添加或删除元素。但不支持在队列中间添加元素。

在 Java SE 6 中引入了 Deque 接口,并由 ArrayDequeLinkedList 类实现。这两个类都提供了双端队列,并且可以在必要时增加队列长度

优先级队列

优先级队列(priority queue) 中的元素可以按任意顺序插入,却总书按照排序顺序进行检索。也就是说,无论何时调用 remove 方法,总会获得当前优先级队列中最小的元素。

然而,优先级队列并没有对所有元素进行排序。优先级队列使用了一个优雅高效的数据结构,从为***堆(heap)***。堆是一个可以不断自我调整的二叉树,对树执行 add 和 remove 操作,可以让最小元素移动到根节点。

与 TreeSet 一样,一个优先级队列既可以保存实现了 Comparable 接口的类对象,也可以保存在构造器中提供的 Comparator 对象。

映射

Java 类库为映射提供了两个通用的实现:HashMapTreeMap 。这两个类都实现了 Map 接口。

基本映射操纵

散列映射(HashMap) 对键进行散列,树映射用键的整体顺序对元素进行排序,并将其组织成搜索树。散列的比较函数只能作用于键。与键关联的值不能进行散列或比较。

例如为存储员工学习建立一个散列映射:

Map<String, Employee> map = new HashMap<>();
Employee cygao = new Employee("cygao", 9200);
map.put("660-12", cygao);

映射添加对象时,必须提供一个键,上面的键约束一个字符串,要检索对象时,必须提供一个键

String id = "600-12";
Employee e = map.get(id);

如果映射中没有与给定键对应的学习,get 将返回null。null 返回值可能并不方便,可以用 getOrDefault 方法设定默认的返回值。键必须是唯一的,不能对一个键同时存放两个值。

remove 方法用于从映射中删除给定键对应的元素并返回。

要迭代处理映射的键和值,最容易的方法是使用 forEach 方法。

map.forEach((k, v) -> System.out.println("key=" + k + " value=" + v));
更新映射

更新映射项是一个难点,要考虑一个键是否是第一次出现的情况,一个单词计数器的例子:

Map<String, Integer> counter = new HashMap<>();
String word = "student";

// 方法一:
counter.put(word, counter.getOrDefault(word, 1));

// 方法二:
counter.putIfAbsent(word, 0);
counter.put(word, counter.get(word) + 1);

// 方法三:
counter.merge(word, 1, Integer::sum);
方法说明
default V merge(K key, V value, BiFunction<? super V, ? super V, ? extends V> remappingFunction)如果 key 与一个非 null 的 v 有关联,那么将该函数应用到 v 和 value 上,如果结果为 null 则删除这个键。否则,将 key 和 value 关联。返回 get(key)
default V compute(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction)将函数应用到 key 和 get(key)。将 key 与结果关联,或者如果结果为 null,则删除这个键。返回 get(key)
computeIfAbsent如果 key 为 null 则将函数应用 key 和 get(key),其余同上
computeIfPresent如果 key 不为 null 则将函数应用 key 和 get(key),同上
default void replaceAll(BiFunction<? super K, ? super V, ? extends V> remappingFunction)在所有映射上应用函数。如果结果为 null 则将相应的键删除
弱散列映射 WeakHashMap

当映射中的一个键已经失去存在的价值时,就要将其从映射中删除。但垃圾回收器跟踪活动的对象。自己要映射对象是活动的,其中的所有桶也是活动的,它们不能被回收。因此需要程序负责从长期存活的映射表中删除无用的值,或是使用 WeakHashMap 完成这件事。

和 HashMap 一样,WeakHashMap 也是一个散列表,它存储的内容也是键值对(key-value)映射,而且键和值都可以是null

不过 WeakHashMap 的键是弱键,当某个键不再正常使用时,会被从WeakHashMap中被自动移除。准确的说,对于一个给定的键,其映射的存在并不阻止垃圾回收器对该键的丢弃。

弱键 使用的是 弱引用(weakReference) 保存键,当弱键不再被其它对象引用,并被 CG 回收的同时,这个弱键也会添加到 ReferenceQueue 中。

LinkedHashSetLinkedHashMap

LinkedHashSetLinkedHashMap 类可以用来记住插入元素项的顺序。当条目插入到表中时,就会并入到双向链表中。

枚举类与映射

EnumSet 是一个枚举类元素集的高效实现。

enum Weekday {Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday};

// 可以用静态工厂方法构造这个集
EnumSet<Weekday> always = EnumSet.allOf(Weekday.class);
EnumSet<Weekday> never = EnumSet.noneOf(Weekday.class);
EnumSet<Weekday> workdays = EnumSet.range(Weekday.Monday, Weekday.Friday);
EnumSet<Weekday> rand = EnumSet.of(Weekday.Wednesday,Weekday.Sunday,Weekday.Thursday);

EnumMap 是一个键类型为枚举类的映射。它可以直接高效地用一个值数组实现。使用时需要指定键类型:

EnumMap<Weekday, Employee> personInCharge = new EnumMap<>(Weekday.class);
标识散列映射 IdentityHashMap

并发

线程基本概念

用户每启动一个进程,操作系统就会为该进程分配一个独立的内存空间。线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程本身不拥有系统资源,只拥有一点再运行中必不可少的资源,但它可用于同属一个进程的其它线程共享进程所拥有的全部资源。

  • 线程是轻量级的进程
  • 线程没有独立的地址空间(内存空间)
  • 线程是由进程创建
  • 一个进程可以拥有多个线程
线程状态

Thread类中有一个State枚举类型列举了线程的所有状态

  • New (新创建)
  • Runnable (可运行)
  • Blocked (被阻塞)
  • Waiting (等待)
  • Timed Waiting (计时等待)
  • Terminated (被终止)

调用 Thread.sleep 方法不会创建一个新县城,sleep 是 Thread 类的静态方法,用于暂停当前线程的活动。

线程的创建要继承 Thread 类或者实现 Runnable 接口。

警告⚠️:不要调用 Thread 类或者 Runnable 对象的 run 方法。直接调用 run 方法,只会执行一个线程中的任务,而不会启动新线程。应该调用 Thread.start 方法。这个方法将创建一个执行 run 方法的新线程。

一、继承Thread类
  编写简单,可直接操作线程
  适用于单继承
二、实现Runnable接口
  避免单继承局限性
  便于共享资源

中断线程

当线程的 run 方法执行方法体中最后一条语句后,并经由执行 return 语句返回时,或者出现了在方法中没有捕获的异常时,线程将终止。

interrupt 方法可以用来请求终止线程。当对一个线程调用 interrupt 方法时,线程的中断状态将被置位。可以调用 isInterrupted 方法判断线程是否被中断。

while (Thread.currentThread().isInterrupted()) 
{
    do more work
}

注⚠️: 有两个非常相似的方法,interruptedisInterruptedInterrupt 方法是一个静态方法,用于检测当前线程是否被中断,并清除该线程的中断状态。而 isInterrupted 方法是一个实例方法,用于与检测是否有线程被中断,且不会改变中断状态。

一点调用 start 方法,线程将处于 runnable 状态。

线程属性

  • 线程优先级
  • 守护线程
  • 线程组
线程优先级

默认情况下,一个线程继承其父线程的优先级。可以用 setPriority 方法提高或降低任何一个线程的优先级。优先级课设为 MIN_PRIORITY(1) 与 MAX_PRIORITY(10) 之间的任何值。但是,线程优先级是高度依赖于系统的

警告⚠️: 如果有几个高优先级的线程没有进入非活动状态,低优先级的线程可能永远也不能执行。

守护线程

可以通过调用 t.setDeamon(true) 将线程转换为***守护线程***。守护线程的唯一用途是为其它线程提供服务。当线程只剩下守护线程的时候,JVM就会退出。

意义及应用场景

当主线程结束时,结束其余的子线程(守护线程)自动关闭,就免去了还要继续关闭子线程的麻烦。如:Java垃圾回收线程就是一个典型的守护线程;内存资源或者线程的管理,但是非守护线程也可以。

未捕获异常处理机制

多线程运行不能抛出任何受查异常,异常会被直接抛出到控制台,也就是说 try-catch 语句不会执行 catch 部分。

为了在多线程运行中捕获异常,在线程死亡之前,异常会被传递到一个用于未捕获异常的处理器。

该处理器必须属于一个实现 Thread.UncaughtExceptionHandler 接口的类。这个接口只有一个方法 ***void uncaughtException(Thread t, Throwable e)***,例如:

class MyUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler {
  @Override
  public void uncaughtException(Thread t, Throwable e) {
    System.out.println(this.getClass().toString());
  }
}

可以用 setUncaughtExceptionHandler 方法为任何线程安装一个处理器。也可以用 Thread.setDefaultUncaughtExceptionHandler 方法为所有线程安装一个处理器。例如:

ThreadDemo t1 = new ThreadDemo();
t1.setUncaughtExceptionHandler(new MyUncaughtExceptionHandler());   // 为t1安装一个处理器

Thread.setDefaultUncaughtExceptionHandler(new MyUncaughtExceptionHandler());   // 设置默认处理器

如果不安装默认的处理器,默认处理器为空。但是,如果不为独立的线程安装处理器,次数处理器就是该线程的 ThreadGroup 对象。

同步

在大多数实际的多线程应用中,两个或以上的线程需要共享统一数据的存取。如果两个线程同时操作,可能会产生讹误的对象。这样一个情况通常称为***竞争条件(race condition)***。

竞争条件

在 Java 多线程中,当两个或以上的线程对同一个数据进行操作时,可能会产生竞争条件的现象。

这种现象产生的根本原因是因为多个线程在对同一个数据进行操作,此时对该数据的操作是非“原子化”的,可能前一个线程对数据的操作还没有结束,后一个线程又开始对同样的数据开始进行操作,这就可能会造成数据结果的变化未知。

线程锁

如果在一个线程对数据进行操作的时候,禁止另外一个线程操作此数据,那么,就能很好的解决以上的问题了。这种操作叫做给线程加锁。

class SafeBank {
  private Lock bankLock = new ReentrantLock();   // 创建一个锁对象

  private final double[] accounts;

  public SafeBank(int num, double initialBalance) {
    accounts = new double[num];
    Arrays.fill(accounts, initialBalance);
  }

  public void transfer(int from, int to, double amount) {
    bankLock.lock();   // lock
    try {
      System.out.print(Thread.currentThread());
      accounts[from] -= amount;
      System.out.printf("%10.2f from %d to %d ", amount, from, to);
      accounts[to] += amount;
      System.out.printf("Total Balance: %10.2f\n", getTotalBalance());
    }
    finally {
      bankLock.unlock();   // make sure the lock is unlocked even if an exception is thrown
    }
  }

  private double getTotalBalance() {
    double sum = 0;
    for (double i : accounts) sum += i;
    return sum;
  }

  public final int size() {
    return accounts.length;
  }
}

每一个 Bank 对象都有自己的 ReentrantLock 对象。如果两个线程试图访问同一个 Bank 对象,那么锁将以串行的方式提供服务。

ReentrantLock 是可重入锁,synchronized 也是,可重入锁,指的是以线程为单位,当一个线程获取对象锁之后,这个线程可以再次获取本对象上的锁,而其他的线程是不可以的。

可重入锁的意义之一在于防止死锁。实现原理实现是通过为每个锁关联一个请求计数器和一个占有它的线程。当计数为0时,认为锁是未被占有的;线程请求一个未被占有的锁时,JVM将记录锁的占有者,并且将请求计数器置为1 。如果同一个线程再次请求这个锁,计数器将递增。每次占用线程退出同步块,计数器值将递减。直到计数器为0,锁被释放。

警告⚠️:把解锁操作放在 finally 子句中是非常重要的。如果在临界区的代码抛出异常,锁必须被释放。否则,其它线程将永远阻塞。

注释📓:如果使用锁,就不能使用带资源的 try 语句

条件对象

线程进入临界区,却发现在某一条件满足后它才能执行。要使用一个条件对象来实现。利用英航账户资金转出:

if (bank.getBalance(from) >= amount)
    bank.transfer(from, to, amount)   // WARNING!

注意不能使用如上代码,当前线程完全有可能在完成判断后被中断!必须保证没有其它线程在检索余额与转账之间修改余额。可以通过锁来保护检查与转账操作:

public SafeConditionBank(int num, double initialBalance) {
    sufficientFunds = bankLock.newCondition();   // 条件对象
    ...
}
public void transfer(int from, int to, double amount) {
    bankLock.lock();
    try {
        while (accounts[from] < amounts)
            sufficientFunds.await();   // 进入该条件的等待集
        ...
        sufficientFunds.signalAll();   // 解除阻塞
    } catch (InterruptedException e) {
        e.printStackTrace();
        finally {
            bankLock.unlock();
        }
    }
}

一个锁对象可以拥有多个条件对象。

如果转账的金额大于当前账户的余额,那么条件对象 condition 就 await 等待,直到其他账户先转入给它足够的余额。这里的 condition 是通过 ReentrantLock 的 newCondition 方法构造的。

调用了 condition 的 await 后就会放弃锁,同时等待,进入该 condition 的等待集。一旦有其他线程调用了同一个条件对象的 signalAll 方法后,就会重新激活因为这个条件等待的所有的线程。

调用 signalAll 方法时不是立即激活一个等待线程,而是解除了等待线程的阻塞,让它们继续往下执行。

通常,对 await 的调用应该在如下形式的循环体中☑️:

while (!(ok to proceed))
    condition.await();

另一个方法 ***signal***,则是随机解锁等待集中某个线程的阻塞状态。这比解除所有线程的阻塞更有效,也存在危险。谨防出现死锁!

synchronized 关键字

总结一下锁与条件的关键之处:

  • 锁用来保护代码片段,任何时刻只能由一个线程执行被保护的代码
  • 锁可以管理视图进入被保护代码段的线程
  • 锁可以拥有一个或多个相关的条件对象
  • 每个条件对象管理那些已经进入被保护的代码段但还不能运行的线程

synchronized 作为关键字的作用域由两种:

1、在某个对象实例内,synchronized aMethod(){} 可以防止多个线程访问这个对象的 synchronized 方法。这时,不同的对象实例的 synchronized方法是不相干扰的。也就是说,其它线程照样可以同时访问相同类的另一个对象实例中的synchronized方法;

2、某个类的范围,synchronized static aStaticMethod{} 防止多个线程同时访问这个类中的synchronized static 方法。它可以对类的所有对象实例起作用。

synchronized 代码块

除了方法前用 synchronized 关键字,synchronized 关键字还可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。

synchronized 的弊端

同步方法,这时synchronized锁定的是哪个对象呢?它锁定的是调用这个同步方法对象。也就是说,当一个对象 P1在不同的线程中执行这个同步方法时,它们之间会形成互斥,达到同步的效果。但是这个对象所属的Class所产生的另一对象P2却可以任意调用这个被加 了synchronized关键字的方法.同步方法实质是将synchronized作用于object reference。――那个拿到了P1对象锁的线程,才可以调用P1的同步方法,而对P2而言,P1这个锁与它毫不相干,程序也可能在这种情形下摆脱同步机制的控制,造成数据混乱:(

若将一个大的方法声明为synchronized 将会大大影响效率,典型地,若将线程类的方法 run() 声明为 synchronized ,由于在线程的整个生命期内它一直在运行,因此将导致它对本类任何 synchronized 方法的调用都永远不会成功。当然我们可以通过将访问类成员变量的代码放到专门的方法中,将其声明为 synchronized ,并在主方法中调用来解决这一问题,但是 Java 为我们提供了更好的解决办法,那就是 synchronized 块。

volatile

**原子性:**在Java中,对基本数据类型的变量的读取和赋值操作是原子性操作,即这些操作是不可被中断的,要么执行,要么不执行。

**可见性:**保证被修改的值能立即更新到主存,当其它线程需要读取时,会到内存中读取新值。

有序性: 禁止指令重排序优化,普通的变量仅仅会保证在该方法的执行过程中所有依赖该赋值结果的地方都能获得正确的结果,而不能保证变量赋值操作的顺序与程序代码中的执行顺序一直。

一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:

  • 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
  • 禁止进行指令重排序。
//线程1
boolean stop = false;
while(!stop){
    doSomething();
}
//线程2
stop = true;

那么当线程2更改了stop变量的值之后,但是还没来得及写入主存当中,线程2转去做其他事情了,那么线程1由于不知道线程2对stop变量的更改,因此还会一直循环下去。

但是用volatile修饰之后就变得不一样了:

  • 使用 volatile 关键字会强制将修改的值立即写入主存
  • 使用 volatile 关键字当线程2修改时,会导致线程1的工作内存中缓存变量stop的缓存行无效
  • 由于线程1的工作内存中缓存变量stop的缓存行无效,所以线程1再次读取变量stop的值时会去主存读取。

volatile 关键字⚠️无法保证原子性,但能在一定程度上保证有序性。

final 变量

对于final域,编译器和处理器要遵循两个重拍序规则:

  • 在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。

  • 初次读一个包含final域的对象的应用,与随后初次读这个final域,这两个操作之间不能重排序。

死锁

所谓*死锁(deadlock)*是指两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。

线程局部变量

线程间有时要避免共享变量,可以使用 ThreadLocal 辅助类为各个线程提供各自的实例。

例如,SimpleDateFormat 类是线程不安全的:

public static final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");

如果两个线程都执行以下操作:

String dateStamp = dateFormat.format(new Date());

结果可能会很混乱,因为dateFormat使用的内部数据结构可能会被并发的访问所破坏。或者也可以在需要时构造一个局部SimpleDateFormat对象,不过这也太浪费了。

要为每个实例构造一个实例,可以使用以下代码:

public static final ThreadLocal<SimpleDateFormat> dateFormat = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));

在一个给定线程中首次调用get时,会调用initialValue方法。在此之后,get方法会返回属于当前线程的那个实例。

锁超时

线程在调用lock方法来获得另一个线程所持有的锁的时候,很可能发生阻塞。应该更加谨慎地申请锁。try Lock方法试图申请一个锁,在成功获得锁后返回true,否则,立即返回false,而且线程可以立即离开去做其他事。

读 / 写锁

ReentrantReadWriteLock 类

阻塞队列

阻塞队列(BlockingQueue)是一种队列,一种可以在多线程环境下使用,并且支持阻塞等待的队列。和一般队列的区别在于:

  • 多线程环境支持,多个线程可以安全的访问队列
  • 支持生产和消费等待,多个线程之间互相配合,当队列为空的时候,消费线程会阻塞等待队列不为空;当队列满了的时候,生产线 程就会阻塞直到队列不满。
方法/处理方式抛出异常返回特殊值一直阻塞超时退出
插入方法add(e)offer(e)put(e)offer(e,time,unit)
移除方法remove()poll()take()poll(time,unit)
检测方法element()peel()不可用不可用

JDK7 提供了 7 个阻塞队列。分别是

名称描述
ArrayBlockingQueue一个由数组结构组成的有界阻塞队列。(底层是数组)
LinkedBlockingQueue一个由链表结构组成的有界阻塞队列。(底层是链表)
PriorityBlockingQueue一个支持优先级排序的无界阻塞队列。
DelayQueue一个使用优先级队列实现的无界阻塞队列。
SynchronousQueue一个不存储元素的阻塞队列。
LinkedTransferQueue一个由链表结构组成的无界阻塞队列。
LinkedBlockingDeque一个由链表结构组成的双向阻塞队列

线程安全的集合

早期线程安全的集合有:VectorHashTable

Java SE 8 的流库

流提供了一种让我们可以在比集合更高的概念级别上指定计算的数据视图。

从迭代到流的操作

例如我们对长单词进行计数操作,我们通常会遍历它的元素,并在每个元素上执行某项操作。但是我们可以使用流进行更高效的操作:

String[] strings = {"11", "22", "###"};
List<String> list = Arrays.asList(strings);
long count = list.stream().filter(w -> w.length() >= 3).count();

仅将 stream 修改为 parallelStream 就可以让流库以并行方式来执行过滤和计数。

long count = list.parallelStream().filter(w -> w.length() >= 3).count();

流看起来和集合很类似,都可以让我转换和获取数据。但是,它们之间存在着显著的差异:

  • 流并不存储其元素。这些元素可能存储在底层的集合中,或者是按需生成的。
  • 流的操作不会修改其数据源。
  • 流的操作是尽可能惰性操作的。

流的创建

Collections 接口的 stream 方法可以将任何集合转换为一个流。

如果你有一个数组,那么可以使用静态的 Stream.of 方法转换为一个流:

Stream<String> words = Stream.of(list);

使用 Arrays.stream(array, from, to) 可以从数组 from 到 to 的元素创建一个流:

String[] strings = {"11", "22", "###"};
Stream<String> words = Arrays.stream(strings, 1, 3);

创建不包含任何元素的流,可以使用静态的 Stream.empty 方法:

Stream<String> silence = Stream.empty();

Stream 接口有两个用于创建无限流的静态方法,generateiterate 方法。generate 方法接受一个不包含任何引元的函数(Supplier 接口),通过反复调用函数生成一个无限流:

Stream<String> echos = Stream.generate(() -> "Echo");
// 或者像下面这样获取一个随机数的流
Stream<String> randoms = Stream.generate(Math::random);

为了产生无限序列例如1 2 3…,可以使用 iterate 方法。它会接受一个“种子”以及一个函数(UnaryOperation),并且会反复讲该函数应用到之前的结果上:

Stream<BigInteger> integers = Stream.iterate(BigInteger.ZERO, n -> n.add(BigInteger.ONE));

filter、map 和 flatMap 方法

filter 的引元是 Predicate ,即从 T 到 boolean 的函数。filter 转换会产生一个流,它的元素与某种条件相匹配。

map 类似 Python 中的 map() 函数,参数接受一个转换函数,会讲每一个函数应用到每个元素上,通常我们可以用 lambda 表达式替代:

Integer[] k = {4, 6, 8, 10};
System.out.println(Arrays.toString(Arrays.stream(k).map(w -> w / 2).toArray()));

抽取连接流和子流

方法描述
Stream<T> limit(long maxSize)产生一个包含当前六中最初的 maxSize 个元素的流
Stream<T> skip(long n)产生一个除了前 n 个元素之外的元素的流
static <T> Stream<T> concat(Stream<? extends T> a, Stream<? extends T> b)产生一个连接前后两个流的流

其它的流转换

方法描述
Stream<T> distinct()流去重
Stream<T> sorted()
Stream<T> sorted(Comparator<? super T> comparator)产生一个可以对元素排序的流
Stream<T> peek(Consumer<? super T> action)产生一个一模一样的流,但是会对每一个元素调用一个函数,对于调试来说很方便

简单约简

约简是一种终结操作(terminal operation),它们会将流约简为可以在程序中使用的非流值。

例如 count 方法 还有 max min 等。这些返回的都是一个类型为 Optional 的值,它要么包装了答案,要么表示没有任何值。例如获得流中的最大值:

Optional<String> largest = words.max(String::compareTo);
System.out.println("Largest: " + largest.orElse(""));
方法描述
Optional<T> max(Comparator<? super T> comparator)分别产生这个流的最大和最小元素,由比较器定义排序规则,如果这个流为空,则产生一个空的 Optional 对象
Optional<T> min(Comparator<? super T> comparator)这些操作都是终结操作
Optional<T> findFirst()分别产生这个流的第一个和任意一个元素,如果这个流为空,则产生一个空的 Optional 对象
Optional<T> findAny()这些操作都是终结操作
boolean anyMatch(Predicate<? super T> predicate)分别在这个流中任意元素、所有元素、没有元素匹配时返回true
boolean allMatch(Predicate<? super T> predicate)这些操作都是终结操作
boolean noneMatch(Predicate<? super T> predicate)

Optional 类型

Optional 对象是一种包装器对象,要么包装了类型T,要么没有包装对象。对于第一种情况,我们称这种值为存在的。Optional 类型被当作一种更安全的方式来代替类型 T 的引用,这种引用要么引用某个对象,要么为 null/但是只有在正确使用的情况才更安全。

方法描述
T orElse(T other)产生这个 Optional 的值,若空返回other
T orElseGet(Supplier<? extends T> other)产生这个 Optional 的值,若空返回调用 other 结果
<X extends Throwable> T orElseThrow(Supplier<? extends X> exceptionSupplier)产生这个 Optional 的值,若空返回抛出 exceptionSupplier 的异常
void ifPresent(Consumer<? super T> consumer如果 Optional 不为空,则将值传递给 consumer
map

未完待续。。。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值