On_Java_Advanced_Edition

On_Java_Advanced_Edition

01 枚举类型

1.2 在枚举类型中添加自定义方法

package org.example;

public enum Run_RR {
    YANG("This is most helpful.."),
    QIAN("This is a good test of Enum"),
    TAO("This is a best chance for help poor boys..");

    private String description;

    private Run_RR(String description) {
        this.description = description;
    }

    public String getDescription(){
        return description;
    }
}

这么说,里面的YANG QIAN TAO这种类型其实也就是Run_RR的实例被,很像是一种递归调用的感觉。

重载枚举类型中的方法

package org.example.enums;

import java.util.stream.Stream;

public enum SpaceShip {
    SCOUT, CARGO, TRANSPORT, CRUISER, BATTLESHIP, MOTHERSHIP;

    @Override
    public String toString() {
        String id = name();
        String lower = id.substring(1).toLowerCase();
        return id.charAt(0) + lower;
    }

    public static void main(String[] args) {
        Stream.of(values())
                .forEach(System.out::println);
    }
}

Enum 在编译器内部的类型就类似于这样

enum Spring{
    TAO,
    THIN;
}

和这个 Enum<Spring> 是一样的。

也可以用接口组织枚举。

package MyTools;

import java.util.Arrays;

public class ConstantsEnum {
    public static void main(String[] args) {
        for (var x :
                Constance.CLASSPATH.info().split(";")) {
            System.out.println(x);
        }
    }
}

enum Constance {
    CLASSPATH {
        @Override
        String info() {
            return System.getenv("PATH");
        }
    };

    abstract String info();
}

枚举类型也可以实现方法,不仅限于abstract,也可以用普通的重写方法。你差不多可以把它当作一个内部类使用。

当然,也可以拥有多个构造器

enum Test{
    
    Tess("Look"),
    Tesss(12);
    
    int value;
    String s;
    
    Test(int value){
        this.value = value;
    }
    
    Test(String s){
        this.s = s;
    }
}

JDK17后,switch(){}语句的诞生使得枚举的作用变得越来越多

public class Test {

    public static void main(String[] args) {
        test(new Tye());
    }

    static void test(Try tr) {
        switch (tr) {
            case TT c -> {
                System.out.println("This is TT.c");
            }
            case Tye s -> {
                System.out.println("This is Tye.s");
            }
        }
    }
}

sealed interface Try {
    void show();
}

final class Tye implements Try {
    @Override
    public void show() {
        System.out.println(getClass().getSimpleName());
    }
}

final class TT implements Try {
    @Override
    public void show() {
        System.out.println(getClass().getSimpleName());
    }
}

1.8 用EnumSet来代替标识

性能是他的设计目标之一。其内部实现其实是一个long数组。

EnumSet<Enum> point = Enum.noneOf(Enum.class)

EnumSet<Enum> point = Enum.Of(多个参数都可以,有三到五个的版本,也有变形参数的版本)

这体现了他对于性能的关注。

1.9 使用EnumMap

这是一种特殊的Map,他要求自身的所有键来自于某个枚举类型。由于枚举的约束,EnumMap的内部可以作为一个数组来实现。

因此他们的性能非常好,你可以放心的使用EnumMap来实现基于枚举的查询。

你只能用·枚举·中的元素作为键来调用put()方法。除此之外,就和调用一个普通的Map没什么区别了。

Switch需要注意的地方(自己总结的)

  1. switch支持了箭头语法,JDK14增加了swtich的能力。在箭头语法中,不需要break来断句了。

  2. 箭头语法可以直接成为返回值
    switch(value){ case Test t -> "This is a output.." }
    用print直接就可以把switch输出。

  3. JDK17增加了预览功能,注意是预览功能,现在还是不能用的。
    就是可以在case中使用null这个选项了。

  4. JDK14中也可以把swtich的普通模式(就是使用case 1: System.out.println();break;这种)转变为可以返回值的switch语句
    具体的关键字是yield。

    var i = switch(s){
        case 1 : yield 1;
        case 2 : yield 2;
        case 3 : yield 3;
        default : yield "default";
    }
    System.out.print(i);
    
    var result = switch(s){
        case "i" -> 1;
        case "o" -> 2;
        case "p" -> 3;
        default -> 0;
    }
    
  5. JDK16最终完成了JEP的定稿,下面是新旧两者方法的区别;

    public class Test_2 {
        
        static void dumb(Object x){
            if (x instanceof String){
                String x1 = (String) x;
                System.out.println(x1);
            }
        }
        
        static void smart(Object x){
            if (x instanceof String s && s.length() > 0)
                System.out.println(s);
        }
        
    }
    

    这里面的s被称为 == 模式变量 ==。(但是它有一个极端情况,就是在这个if语句外面也有可能会捕捉到他。虽然这不是一个bug但是最好不要使用)

1.17 新特性:模式匹配

1.17.2 守卫设计模式

用的就是类似于这种语法

static void classify(Shape s) {
    System.out.println(switch (s) {
        case Circle c && c.area() < 100.0 -> "Small Circle" + c;
        case Circle c -> "Large Circle: " + c;
        case Rectangle r && r.side1() == r.side2() -> "Square: " + r;
        case Rectangle r -> "Rectangle: " + r;
    });
}

1.17.3 支配性

switch中的顺序很重要,如果基类先出现,就会支配任何出现在后面的case

就像异常那样,Exception如果放在最前面,那么后面的异常就不会再有机会被捕获。

1.17.4 覆盖范围

模式匹配会引导你逐渐使用sealed关键字,这有助于你覆盖了所有可能传入的选择器表达式类型。

如果待被设计的类们(假如有三个)都使用了继承了一个sealed的类或接口,那么进入switch如果case里面只放了两个,那么就会报错

会强制你添加default子句,来防止有误输入的选项。

02 对象传递和引用

Java实际上是有指针的。

不过这些指针都类似与小学生在玩的安全剪刀,并不锋利。可以安全的使用。但是有时会稍显繁琐。

2.1 传递引用

当你将一个引用传给方法后,该引用指向的仍然是原来的对象。可以通过下面这个简单的实验看出这一点。

public class Pass{
    public static void f(Pass h){
        System.out.println(h);
    }
    public static void main(String[] args){
        Pass p = new Pass();
        
        System.out.println(p);
        f(p);
    }
}

他们两个得到的结果是同一个对象。

这说明,他们在传递的是一个对象的引用而不是一个新的对象。

引用别名

引用别名指的是:不止一个引用被绑定到了同一个对象上的情况。

public class Alias1{
    private int i;
    public Alias1(int i1){
        i = i1;
    }
    public static void main(String[] args){
        Alias1 x = new Alias1(7);
        Alias1 y = x;
        
        System.out.println("x: " + x.i);
        System.out.println("y: " + y.i);
        
        x.i++;
        
        System.out.println("x: " + x.i);
        System.out.println("y: " + y.i);
        
    }
}
x: 7
x: 7
X: 8
X: 8

y并不想被改变,但是它由不得自己。

如果你在方法调用的过程中必须修改参数,同时又不想改变该参数在方法外部的状态,那么就要通过在方法内部复制出一个参数的副本的方式进行保护。这是本章的大部分内容的主题。

2.2 创建本地副本

  • 引用别名会在传递参数时自动发生
  • 并没有本地对象,只有本地引用
  • 引用是有作用域的,对象则没有
  • Java中的对象生命周期从来就不是个问题。

许多编程语言支持 在方法内 自动创建外部对象 的本地副本 的能力。

Java并不支持这样做,但是它可以让你实现同样的效果。

犯了一个奇怪的小脑筋:

让我难过了一个晚上

ArrayList<? super Toys> toys = new ArrayList<>();

这说明toys里面全部都是Toys的父类,同时也把里面的类型全部看作Object,这样的话,里面的元素是无法调用函数里面自带的方法的。

2.2.2 克隆对象

引用Cloneable接口的直接重写的类都是浅拷贝

什么是浅拷贝呢

关于这个我认为还是人类语言描述的好,适当的配点代码说明一下

package fun.example;

public class test implements Cloneable {
    private int i = 1;
    private text1 k;

    public test(text1 k) {
        this.k = k;
    }

    @Override
    public test clone() {
        try {
            return (test) super.clone();
        } catch (CloneNotSupportedException e) {
            throw new RuntimeException();
        }
    }

    public static void main(String[] args) {
        test test_1 = new test(new text1());
        System.out.println(test_1+"\n============");

        test clone = test_1.clone();
        System.out.println(clone);

        clone.k.i = 11;

        System.out.println(test_1);
    }

    @Override
    public String toString() {
        return "test{" +
                "\ni=" + i +
                ",\nk=" + k +
                '}';
    }
}


class text1 {
    public  int i = 1;

    @Override
    public String toString() {
        return "text1{" +
                "i=" + i +
                '}';
    }
}

什么浅拷贝呢,就是只复制了数据的第一层,这样说可能有点抽象。

例子中的test1类就是一个被完全克隆的对象,我可以随便改变他的。

但是问题是,他下面的数据其实是两个对象共享的,两个对象(克隆的以及被克隆的)其实用的是同一个引用链接。

也就是说,修改一个的同时,等于修改了两个。从System.out.println();可以看出来。

2.2.3 为类增加可克隆的能力

  • 调用super.clone()
  • 将你的克隆操作设为public的
  • 实现Cloneable接口(据作者所说,这没有什么用,就是单纯的强迫你加上,因为Java在Object.clone()里面会强制检查这个接口,但他本身并没有什么作用)。
  • Object.clone()会检测该对象有多大,并为新对象创建足够多的内存空间,然后将旧的对象的所有二进制位都复制到新的对象中,这称为按位复制
  • 如果你不重写这个clone方法的话呢,是不会用有这个能力的;
  • 如果你重写了,也就是显示的定义了这个方法的实现,那么他的所有子类也都将会拥有这个能力。
  • 如果你想限制他的子类的克隆能力或者不想让他被复制,那么请使用final类

由此我们甚至得出了一个结论:

==运算符其实比较的就是类的引用是否相等!!

2.2.6 克隆组合对象

也就是深度克隆

实际上并没有什么捷径可以直接全部克隆。

也就是在重写的clone方法里,调用它里面的其他对象的clone方法罢了(也就是将他的元素调用一遍clone)。实际上是没有什么技术含量的。

这里预习了一下Java序列化的东西:Java序列化是指把Java对象转为字符序列的过程。

2.2.9 在继承层次结构中增加可克隆性并向下覆盖

如果你创建一个新类,其基类默认是Object,因此默认是不具备可克隆性的,只要你不显性的增加该能力,他就不会具备。

但你在继承层次结构中的任意一层增加该能力,而且从该层开始向下的所有层次都会具备该能力。

2.3 控制可克隆性

  1. 不关心。
  2. 支持clone。实现接口并重写方法。
  3. 视情况克隆。
  4. 不实现接口,但是将clone重写为protected的。
  5. 通过不实现Cloneable接口并重写clone,使其抛出异常来尽量避免克隆。
  6. 将类定义为final的。以此阻止克隆。

Java中,我们操纵的一切都是引用。

不可变类就是,调用这个变量的时候,用来计算了一大堆东西,而他自己的值却没有发生变化,传递出去的都是新的对象的值。

2.4.2 String很特殊

String本质上是不可变的。

如果想在原基础上直接修改字符串,只能用StringBuilder或者StringBuffer

03 集合主题

3.2 List的行为

add() 用于插入元素
get() 用于随机访问元素 注意这个操作在特定的List实现上成本不同
iterator() 用于返回该序列上的Iterator
stream() 用于生成序列中元素的stream
addAll() 插入一个集合
addAll(3,list) 在位置处3开始 插入一个list
contains("1") 列表中是否包含该元素
containsAll(list) 作为参数的这个列表是否包含在该列表中    
listIterator() 返回一个listIterator
listIterator(3) 从位置3处开始
indexOf("1") 确定对象的索引
isEmpty() 列表中是否存在元素
lastIndexOf("1") 最后一个与参数匹配的元素的索引
remove(1) 移除位置1处的元素
remove("2") 移除该对象
set(1,"y") 将位置1处的元素设置为 "y"
retainAll(List) 对两个集合求交集
removeAll(List) 移除该集合 --> 参数集合里面的元素
size() 该集合的大小
clear() 移除所有元素

List的构造器总是保存插入的顺序

下面是LinkedList才会有的操作

addFirst("one") 压栈
getFirst() 类似于在栈顶执行peek操作
removeFirst() 类似从栈顶弹出
removeLast() 将其看作一个对列,从队尾取出元素

下面是总结一下 ListIterator 的操作和 Iterator 的复习

Iterator 只能向前移动,但是 ListIterator 可以双向移动,

boolean hasNext();     //检查是否有下一个元素
E next();              //返回下一个元素
boolean hasPrevious(); //check是否有previous(前一个)element(元素)
E previous();          //返回previous element(前一个元素)
int nextIndex();       //返回下一element的Index(返回该元素的索引)
int previousIndex();   //返回当前元素的Index
void remove();         //移除一个elment
void set(E e);         //set()方法替换访问过的最后一个元素 注意用set设置的是List列表的原始值
void add(E e);         //添加一个element

3.3 Set的行为

如果不关心元素的顺序或者是并发 HashSet总是最好的选择 因为他就是为了尽可能的快的查找而实现的 (使用了Hash函数)

HashSet的输出看上去没有明显的顺序

TreeSetConcurrentSkipListSet都会对元素进行排序,而且都实现了SortedSet接口来表明这一点 因为Set是有序的 因此他们支持更多的操作

comparator() 生成用于该Set的比较器对象;如果选择自然排序,则返回null。
first() 生成最小元素
last() 生成最大元素
subSet(fromElement,toElement) 声称该Set的一个视图,包含从参数1到参数2(不包含参数2)的元素
headSet(toElement) 生成该Set的一个视图包含小于toElement的所有元素
tailSet(fromElement) 生成该Set的一个视图,包含大于或等于fromElement的所有元素

注意:SortedSet意味着“根据该对象的比较函数来排序”,而不是根据“插入顺序”

如果想要保留插入顺序的话,可以使用LinkedHashSet。

LinkedHashSetCopyOnWriteArraySet会保留元素插入的顺序 尽管没有接口表明这一点

CopyOnWriteArraySetConcurrentSkipListSet是线程安全的

3.4 在Map上使用函数式操作

和Collection接口一样 Map接口内部也内置了forEach()

但是我们如果想执行像map() flatMap() reduce() 或者是 filter() 等其他基本操作

可以通过entrySet()连接到这些方法 他会生成一个由Map.Entry对象组成的Set

这个Set又包含了stream() 和 parallelStream()方法 只需要记住 我们在使用的是Map.Entry的对象

TreeMap和ConcurrentSkipListMap都实现了NavigableMap接口

这个接口的目的就是解决需要选择某个Map中部分元素的问题

NavigableMap的键是有顺序的 所以它支持 firstEntry() 和 lastEntry 的概念

headMap(参数1,true or false) 生成了Map中从开头到第一个参数所指的元素的所有元素第二个参数用于指示结果中是否包含第一个参数所指的元素

tailMap(参数1,true or false) 这个函数的作用和上面的差不多一样,唯一的区别就是,他是从末尾开始的,上面那个是从开头开始的

subMap() 支持生成该Map中间的某段
    
show(Color.subMap(Object1,true,Object2,false)) 生成中间的某段,这个true和false和上面的逻辑一样.

3.10 Set与储存顺序

HashSet 对于查找速度要求比较高的Set. 要添加要添加的元素必须定义hashCode()equal()方法

TreeSet 有序的Set 底层是一个树结构. 通过这种方式 我们可以从某个Set提取出一个有序的序列 要添加的元素必须实现Comparable接口

LinkedHashSet 拥有HashSet的查找速度,但是内部使用了一个链表维护着我们添加元素的顺序(即是插入顺序)。因此,我们在这个Set上迭代时 结果是以插入顺序出现的。要加入的元素必须定义了HashCode()equal()方法

如果我们没有按照上面的来定义自己放入的对象,那么就会出现意想不到的问题。

3.11 Queue

Queue的实现有很多种,其中大部分是为了并发而实现的。有些实现的差别在于排序行为而不是性能。

offer()就是Collection的add()

remove()就是弹出,删除并返回。

我们从一端存入,从另一端取出。

Deque接口也继承自Queue。除了优先级队列以外,Queue都是按照元素的放入顺序来输出元素的.

3.11.1 优先级队列

PriorityQueue<对象> pr = new PriorityQueue<>();

这个类里面必须实现了Comparable接口以及实现了他的方法,compareTo()

3.11.2 Deque

它像一个对列,但是他可以在任意一端添加和移除元素

offerFirst()
offerLast()
pollFirst()
pollLast()

注意,在LinkedBlockingDeque添加元素时,它会在达到其大小限制时停止,然后忽略后续的offer()

3.12 理解Map

3.12.1 性能

哈希码是这样一种方法,他从所处理的对象中提取一些信息,并将这些信息转变为该对象的一个“相对唯一”的int。

HashMap 基于哈希表实现。(使用这个类代替Hashtable)提供了常数时间的键值对插入和定位性能。可以通过构造器来调整其性能,因为构造器支持设置这个哈希表的容量和负载因子。

LinkedHashMap 类似于HashMap,但是在遍历时会以插入的顺序最近最少使用的顺序获得键值对。除遍历以外,性能比HashMap稍低。对于遍历的情况因为它使用了链表来维护内部顺序,所以会更快一点。

TreeMap 基于红黑树实现。当我们查看键或者是键值对时,会发现它们是有序的(顺序是通过comparable或comparator来确定的)。TreeMap的要点是,我们会以有序的方式得到结果。TreeMap是唯一提供了subMap()方法的Map,他会返回树的一部分。

WeakHashMap 由弱键组成的Map,该映射所引用的对象可以被释放。它是为了解决特定类型的问题而设计的。如果在这个映射之外,已经没有指向某个特定键的引用,那么就可以对这些键进行垃圾收集。

ConcurrentHashMap 线程安全的Map,没有使用同步锁,在本书第5章中讨论。

IdentityHashMap 哈希映射,它使用 == 而不是 equal() 来比较键。仅用于解决特殊类型的问题,并非通用。

Map中对于键的要求和Set本身是一样的。

3.12.2 SortedMap

利用SortedMap(TreeMap或ConcurrentSkipListMap实现了该接口),可以确保键是有序的,从而可以使用SortedMap接口中的方法提供的功能。

  • comparator:生成用于该Map的比较器对象;如果使用自然排序,则返回null;
  • firstKey:生成最小的值
  • lastKey:生成最大的值
  • subMap(from , to):生成该Map的一个视图,from有,to没有
  • headMap(toKey):生成一个视图,包含小于toKey的所有键
  • tailMap(fromKey):生成一个视图,包含大于或等于fromKey的所有键

3.12.3 LinkedHashMap

为了提高速度,LinkedHashMap对所有东西都进行了Hash处理,但是在遍历的过程中也会按照插入顺序输出键值对。

此外,可以在LinkedHashMap的构造器中进行配置,让它使用基于访问量的最近最少使用(LRU)算法。

这样的话,尚未访问的元素就会出现到列表的前面(而且因此成为了移除操作的候选)

3.13 工具函数

package fun.utree.Collectiontopics;

import java.util.*;

public class Utilities {
    static List<String> list = Arrays.asList(
            "one two three four five six one".split(" "));

    public static void main(String[] args) {
        System.out.println(list);
        System.out.println("'list' disjoint (Four)?: " +
                Collections.disjoint(list, Collections.singletonList("Four"))); 
        //disjoint()没有共同元素就返回false   singletonList()

        System.out.println("max(): " + Collections.max(list));//使用comparator来计算Collection中最大或最小的元素。
        System.out.println("min(): " + Collections.min(list));//使用comparator来计算Collection中最大或最小的元素。

        System.out.println("max w/ comparator: " +
                Collections.max(list, String.CASE_INSENSITIVE_ORDER));
        System.out.println("min w/ comparator: " +
                Collections.min(list, String.CASE_INSENSITIVE_ORDER));

        List<String> sublist =
                Arrays.asList("Four five six".split(" "));

        System.out.println(
                "indexOfSubList: "
                        + Collections.indexOfSubList(list, sublist)
                //indexOfSubList() 计算sublist在list中第一次出现的位置
                //如果没有出现过,那就返回 -1。
        );

        System.out.println(
                "lastIndexOfSubList: "
                        + Collections.lastIndexOfSubList(list, sublist)
                //lastIndexOfSubList() 计算sublist在list中第一次出现的位置
                //如果没有出现过,那就返回 -1。
        );

        Collections.replaceAll(list, "one", "Yo");
        System.out.println("replaceAll(): " + list);

        Collections.reverse(list);
        System.out.println("reserve(): " + list);

        Collections.rotate(list, 1);
        System.out.println("rotate():" + list);
        Collections.rotate(list, 1);
        System.out.println("rotate():" + list);
        Collections.rotate(list, 1);
        System.out.println("rotate():" + list);
        Collections.rotate(list, 1);
        System.out.println("rotate():" + list);

        List<String> source =
                Arrays.asList("in the matrix".split(" "));

        Collections.copy(list, source);
        System.out.println("copy():" + list);// As we all known , the copy's mean.

        Collections.swap(list, 0, list.size() - 1);// 交换列表中的的0和size()的值。
        System.out.println("swap():" + list);

        Collections.shuffle(list, new Random(47));//随即变换指定的列表
        System.out.println("shuffle():" + list);

        Collections.fill(list, "Pop");
        System.out.println("fill(): " + list);

        System.out.println("frequency of 'Pop' : " + Collections.frequency(list, "Pop"));
        //返回第一个参数(列表)里面与第二个参数相同的数量。

        List<String> dups = Collections.nCopies(3, "snap");
        System.out.println(dups);
        //返回一个不可变列表,里面所有的引用都指向第二个参数。

        System.out.println("'list.disjoint -> 'dups' ? : " + Collections.disjoint(list, dups));
        //如果两个集合没有共同元素,就返回true。
    }
}

3.13.2 创建不可修改的Collection或Map

我们把一个可修改的集合设置为某个类中的private成员,然后从方法里调用返回一个指向该集合的只读引用。

因此我们可以在这个类里修改它,而不在这个类里的只能读取。(视图)

3.14 持有引用

java.lang.ref库里包含一组类,给垃圾收集提供了更大的灵活性。当存在可能耗尽内存的大对象时,这些类特别有用。

有三个继承自抽象类Reference的类:SoftReference WeakReference PhantomReference

我们是这样做的:使用一个Reference对象作为我们和普通引用之间的中介(一个代理)。另外不能存在指向该对象的其他普通引用,(就是没有包在Reference对象中的那种)如果垃圾收集器发现某个对象可以通过普通引用访问到,他就不会释放这个对象。

  • 软引用:实现对内存敏感的缓存。
  • 弱引用:实现规范映射——为节省储存空间,对象的实例可以同时在程序的多个位置使用,这不会妨碍他们的键或是值被回收。
  • 虚引用:以更灵活的方式安排事后清理动作

例子

class VeryBig {
    private static final int SIZE = 10000;
    private long[] la = new long[SIZE];
    private String ident;

    public VeryBig(String ident) {
        this.ident = ident;
    }

    @Override
    public String toString() {
        return ident;
    }


    @SuppressWarnings("deprecation")
    @Override
    protected void finalize() throws Throwable {
        System.out.println("Finalizing " + ident);
    }
}
public class References {
    private static ReferenceQueue<VeryBig> rq = new ReferenceQueue<>();

    public static void checkQueue() {
        Reference<? extends VeryBig> inq = rq.poll();
        if (inq != null)
            System.out.println("In queue : " + inq.get());
    }

    public static void main(String[] args) {
        int size = 10;
        if (args.length > 0)
            size = Integer.valueOf(args[0]);
        LinkedList<SoftReference<VeryBig>> sa = new LinkedList<>();
        for (int i = 0; i < size; i++) {
            sa.add(
                    new SoftReference<>(
                            new VeryBig("Soft " + i), rq));
            System.out.println("Just Created: " + sa.getLast());
            checkQueue();
        }

        LinkedList<WeakReference<VeryBig>> wa = new LinkedList<>();
        for (int i = 0; i < size; i++) {
            wa.add(new WeakReference<>(new VeryBig("Weak " + i), rq));
            System.out.println("Just Created: " + wa.getLast());
            checkQueue();
        }


        SoftReference<VeryBig> s = new SoftReference<>(new VeryBig("Soft"));

        SoftReference<VeryBig> w = new SoftReference<>(new VeryBig("Weak"));

        System.gc();

        LinkedList<PhantomReference<VeryBig>> pa = new LinkedList<>();
        for (int i = 0; i < size; i++) {
            pa.add(new PhantomReference<>(new VeryBig("Phanto" + i), rq));
            System.out.println("Just Created: " + pa.getLast());
            checkQueue();	
            //在这里ReferenceQueue.poll()继续会执行,但是现在的里面的对象已经被清空了。
            //只剩下一堆空壳。那就是null。
            //我们可以知道,当System.gc()执行成功时,虽然对象本身已经没了,但他的引用还在,但是变成了null
        }
    }
}

System.gc();可以直接调用垃圾收集器。如果没有被引用过的那些元素将会被回收。

3.15 Java1.0/1.1的集合类并没有做笔记,作者本身也提倡不要使用。

04 注解

太难了,需要以后在学习,主要新的东西太多了。完全没有接受过。

@Target
@Retention(RetentionPolicy.RUNTIME|CLASS|SOURCE)

05 并发编程

现在我们即将进入并发的世界,你的经验将不再可靠!!

并发是一系列聚焦于如何减少等待并提升性能的技术

一个关键的决定因素是处理器是否多于一个。

如果只有一个处理器,那么它要承受额外的任务切换带来的性能损耗,这时并发反而回事系统变得更慢

只有确定别无选择时,你才能开始使用并发,并且只能在隔离的环境中使用

事实上,从性能角度来看,除非有任务可能阻塞,否则没有理由在单处理器上使用并发。

一切都不可信,一切都很重要

你懂得越少,你以为自己懂得的就越多

你最需要警惕的就是你的自信。

5.7 并行流

即在stream中,使用parallel(),流中的一切就突然都可以作为一组并行的任务来运行了

如果代码使用了Stream,那么就可以轻而易举地通过并行化来提升速度。

并行流十分诱人,你所需要的只是将程序需要解决的问题转换为流,然后插入parallel()来提升速度。

事实上,有时候就是那么简单,但不幸的是,这样做会有很多隐患。

而且parallel()limit()的搭配使用仅限高手。

5.8 创建和运行任务

5.8.1 Task 和 Executor

ExecutorService exec 
    = Executors.newSingleThreadExecutor();
exec.execute();

exec.shutdown();

Thread.currentThread().getName();

首先请注意,不存在SingleThreadExecutor类,这只是他的工厂方法,用于生成特定的类型的ExecutorService类。

我如果将创建的任务提交到了ExecutorService里面,这意味着它们会自行启动。

然而同时,main()方法会继续处理其他的事,当我调用exec.shutdown()时,(他有一个兄弟方法shutdownNow()其作用是不再接受任何新任务,同时还会尝试中断来停止所有正在运行的任务)会告诉ExecutorService完成所有已提交的任务,但不再接受任何新任务。

不过此时那些任务仍然在运行。

SingleThreadExecutor最大的好处就是,里面的人物不会相互影响,因为只有一条线程。

这样的现象称之为线程封闭

5.8.2 使用更多的线程

ExecutorService exec 
    = Executors.CachedThreadPool();

5.8.3 生成结果

多个任务同时修改同一个变量会导致所谓的竞态条件

避免竞态条件的最好办法就是,避免使用可变共享状态。

我们可以称之为自私儿童原则

就是什么都不共享

如果能消除副作用,并且只返回任务结果就好了。

要达到这个目标,我们需要创建一个Callable,而不是Runnable;

public class CountingTask implements Callable<Integer> {
    final int id;

    public CountingTask(int id) {
        this.id = id;
    }

    @Override
    public Integer call() throws Exception {
        Integer val = 0;
        for (int i = 0; i < 100; i++) {
            val++;
        }
        System.out.println(id + " " + Thread.currentThread().getName() + " " + val);
        return val;
    }
}

call()完全独立的生成结果,独立于任何其他的CountingTask,这意味着并不会存在可变共享状态。

ExecutorService允许在集合中通过invokeAll()来启动所有的Callable

public class CachedThreadPool3 {
    public static Integer extractResult(Future<Integer> f) {
        try {
            return f.get();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } catch (ExecutionException e) {
            throw new RuntimeException(e);
        }
    }

    public static void main(String[] args) throws InterruptedException {
        ExecutorService exec =
                Executors.newCachedThreadPool();
        List<CountingTask> tasks =
                IntStream.range(0, 10)
                        .mapToObj(CountingTask::new)
                        .collect(Collectors.toList());
        List<Future<Integer>> futures =
                exec.invokeAll(tasks);		//在所有任务都完成之前,invokeAll甚至不会返回
        Integer sum =
                futures.stream()
                        .map(CachedThreadPool3::extractResult)
                        .reduce(0, Integer::sum);
        System.out.println("sum = " + sum);
        exec.shutdown();
    }
}

Future是Java5中引入的机制,允许你提交一个任务并且不需要等待他完成。

此处,我们使用了ExecutorService.submit()

【1】:当你对尚未完成的任务的Future调用get()方法时,调用会持续阻塞(等待),直到结果可用。

人们并不鼓励使用这个,而是更推荐CompletableFuture。

5.9 终止长时间运行的任务

总结了一句话:我也不知道咋说了:加入后就已经开始运行了。

public class QuitingTasks {
    public static final int COUNT = 150;

    public static void main(String[] args) {
        ExecutorService es =
                Executors.newCachedThreadPool();
        List<QuittableTask> tasks =
                IntStream.range(1, COUNT)
                        .mapToObj(QuittableTask::new)
                        .peek(qt -> es.execute(qt)) 	//从加入开始运行
                        .collect(Collectors.toList());
        new Nap(1);
        tasks.forEach(QuittableTask::quit);
        es.shutdown();
    }
}

5.10 CompletableFuture

public class QuittingCompletable {
    public static void main(String[] args) {
        List<QuittableTask> tasks = IntStream.range(1, QuitingTasks.COUNT)
                .mapToObj(QuittableTask::new)
                .collect(Collectors.toList());
        List<CompletableFuture<Void>> cfutures = tasks.stream()
                .map(CompletableFuture::runAsync) //执行他的里面的run
                .collect(Collectors.toList());
        new Nap(1);
        tasks.forEach(QuittableTask::quit);
        cfutures.forEach(CompletableFuture::join);//等待完成
    }
}

如果不调用join,否则程序就会第一时间推出而不等待多线程的完成。

5.10.1 基本用法

CompletableFuture.completableFuture();

利用这个方法把其中的参数包装成一个对象。

他创建了一个 ”已完成“ 的CompletableFuture。

一般来说,get()会阻塞正在等待结果的被调用线程。该阻塞可以通过InterruptedException或者ExecutionException来退出。

同步调用(即平常我们使用的那种)意味着“完成工作后返回”,而异步调用则意味着立即返回,同时在后台继续工作

就像带着Async的那种方法。

5.10.2 其他操作

CompletableFuture
    .thenAcceptAsync()	//接收 Consumer作为参数
    .thenApplyAsync()	//接收 Function作为参数
    .thenComposeAsync() //和上面的这个很像,但是它的Function必须返回已在CompletableFuture中被包装的结果
    .obtrudeValue()		//强制输入一个值作为结果
    .toCompletableFuture() //以当前的CompletionStage生成CompletableFuture
    
c.complete(9);			//演示了你可以如何通过传入结果来让一个future完成执行(而obtrudeValue可以强制用自己的结果替换)

最后来看看dependent的概念。如果我们将两次对CompletableFuture的thenApplyAsync()调用连在一起,dependent的数量仍然还是一个,但是如果我们直接把另一个thenApplyAsync()添加到c,那么就有了两个dependent:两个连续的调用和一个额外的调用

这说明了一个单独的CompletionStage可以在其完成后,基于他的结果fork出多个新任务。

5.10.3 合并多个CompletableFuture

CompletableFuture中的第二类方法接受两个CompletableFuture做参数,并以多种方式将其合并。一般来说其中一个会先于另一个执行,两者看起来就像是在彼此竞争。这些方法可以让你以不同的方式处理结果。

记忆方法

  1. then 直接
  2. combine 合并
  3. either 其一
  4. both 同时

同步异步

  1. Sync 同步
  2. Async 使用默认的ForkJoinPool
  3. Async 使用自定义Executor

计算类型

  1. Function apply()
  2. Consumer accept()
  3. Runnable run()

combine:结合,合并

5.10.5 异常

CompletableFuture不仅会保存对象,还会缓存异常。

异常只会在get的时候才会显露出来。或者join也一样。

isCompletedExceptionally():你可以先检查处理过程中是否有异常抛出,而不必真的抛出异常。

cfi.completeExceptionally(new Exception("Say Something...")):插入异常,无论是否出现任何失败。

相比于在合并或获取结果时粗暴地使用try-catch,我们更倾向于利用CompletableFuture所带来的更为先进的机制来自动响应异常。

一共有三个选项

  1. exceptionally(
    (ex)->return new Object()):只有出现异常的时候,参数才会运行,他的限制在于Function的返回值类型必须与输入相同。
    将一个正确的对象插回到流,可使这个错恢复到正常状态。
  2. handle((result,fail)->{
    }):第一个参数是结果,第二个是异常。这个函数总是会被调用的,而且你必须检查fail是否为true以确定有异常发生。但是这个函数可以生成任意新类型,因此它允许你执行处理,而不是像上面的那个只是恢复。
  3. whenComplete():和上面的一样,不过它没有返回值的限制,也是每次都会运行都要检测。

5.11 死锁

我们观察到,出现死锁需要同时满足以下4个条件

  1. 互斥。
  2. 至少一个任务必须持有一项资源,并且在等待正被另一个任务持有的资源。
  3. 不能从一个任务中抢走一项资源
  4. 会发生循环等待,其中一个任务等待另一个任务持有的资源,另一个任务又在等待另一个任务持有的资源。
    以此类推,直到某个任务正在等待第一个任务持有的资源,由此一切都陷入了死循环。

5.12 构造器并不是线程安全的

5.14 并发总结

并行流和CompletableFuture是Java并发工具里最发达的技术。

无论何时你都应该优先选择其中之一。

CompletableFuture看起来更像是面向任务的,而不是面向数据。

你可以发现程序有问题,但你永远无法证明他没问题,这是众所周知的并发准则之一。

不论用某种语言或库实现并发看起来多么简单,你都要视其为黑魔法。

如果不给予其足够的重视,总有一天要吃亏的。

06 底层并发

包括main()在内所有代码都运行在某个线程之内。

事实证明,线程通常最佳的数量就是可用的处理器的数量。

每个核心可作为两个超线程。

6.1.2 我可以创建多少线程

只要你一直向CacheadThreadPool传入任务,他就不会不断地创建Thread。

execute()传入对象即可开启任务,分配一个新的Thread。

真正该问的问题是“我需要多少线程

“工作窃取” 线程池Executors.newWorkStealingPool()

这个算法使得已完成自身输入队列中所有工作项的线程可以窃取其他队列中的工作项。

6.2 捕获异常

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class SwallowedException {
    public static void main(String[] args) {
        ExecutorService exec = Executors.newSingleThreadExecutor();
        exec.submit(() -> {
            throw new RuntimeException();
        });
        exec.shutdown();
    }
}

你可能并不会相信,这段代码仿佛会抛出异常,但实际上没有。

因为这段代码的exec.submit是配合它的返回值Future使用的。如果没有调用他的get方法,根本无法捕获他的异常。

你无法捕获已经逃出线程的异常

就算是在外面加上try-catch语句也无法捕获,因为该异常根本不是本线程(main)的异常。

Thread.UncaughtExceptionHandle接口会向每个Thread对象添加异常处理程序。

import java.util.concurrent.*;

class ExceptionThread2 implements Runnable {
  @Override public void run() {
    Thread t = Thread.currentThread();
    System.out.println("run() by " + t.getName());
    System.out.println(
      "eh = " + t.getUncaughtExceptionHandler());
    throw new RuntimeException();
  }
}

class MyUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler {
  @Override
  public void uncaughtException(Thread t, Throwable e) {
    System.out.println("caught " + e);
  }
}

class HandlerThreadFactory implements ThreadFactory {  
    
    //这是用来构造CachedThreadPool的参数,里面的每个线程会自动地按照这个方法来定义自己的线程
 	
    @Override
    public Thread newThread(Runnable r) { 
    //这个 r 貌似就是目前的这个线程。然后方法内部就是给这个线程添加一些自定义的过程
    System.out.println(this + " creating new Thread");
    Thread t = new Thread(r);
    System.out.println("created " + t);
    t.setUncaughtExceptionHandler(
      new MyUncaughtExceptionHandler());
    System.out.println(
      "eh = " + t.getUncaughtExceptionHandler());
    return t;
  }
}

public class CaptureUncaughtException {
  public static void main(String[] args) {
    ExecutorService exec =
      Executors.newCachedThreadPool(new HandlerThreadFactory());//这里已经放入了
    exec.execute(new ExceptionThread2());
    exec.shutdown();
  }
}

上例是基于具体问题具体分析的方式设置处理器,如果你确定要在所有地方都应用同一个异常处理程序,更简单的方法是设置默认未捕获异常处理程序Thread.setDefaultUncaughtExceptionHandler()

只有在为线程专门设置的未捕获异常处理程序不存在的时候,该处理器(setDefaultUncaughtExceptionHandler)才会被调用。

可以将该方法和CompletableFuture里的方法进行比较。

6.3 共享资源

要注意,自增操作本身也包括很多个步骤,而任务有可能在自增操作中途被线程机制挂起。

也就是说,Java的自增操作并不是原子操作,因此即使是最简单的自增操作,如果不对线程进行必要的保护,也是不安全的。

例子:

想象一下你在餐桌上吃饭,手里拿着一根叉子,你正要夹起盘中最后一块食物,食物突然消失了。

这是因为你的线程被挂起了,然后另一位食客进入并吃掉了食物。

避免这种冲突的关键就是在一个任务使用一项资源的时候,对该资源上锁。

所有对象都会自动地包含一个锁【也称为监视器】。在调用任何一个synchronized方法的时候,该对象都会被锁上

该对象的任何其他的synchronized方法都无法被调用。直到第一个调用并释放这个锁。

synchronized void f();
synchronized void g();

如果一个任务调用了某对象的f()方法,另一个任务则无法调用同一个对象上的f()或者g()

尤其重要的是,在使用并发时,要将字段都设置为private的。否则synchronized关键字便无法阻止其他的任务直接访问字段。这样便产生了冲突。

一个线程可以多次获得一个对象的锁。如果一个方法调用同一个对象上的第二个方法,而第二个方法又调用了同一个对象上的另一个方法,便会发生这种情况。以此类推,JVM会持续对对象被上锁的次数进行计数。

当然,只有首先获得锁的线程才被允许多次获得锁。线程每跳出一次synchronized方法,该计数就会减一,直到减为0。

此时完全释放掉锁,让其他线程使用。

6.4 volatile关键字

字分裂出现在你的数据类型足够大(如Java中的long与double,两者都是64位),对某个变量的写操作分为两个步骤的时候。JVM允许将对64位数的读写操作分为两次对32位的数的独立操作。

注意:这有可能在64位处理器上不会发生,也就不会出现这种问题。

volatile的原理:每个线程都可以在处理器缓存中保存变量的本地副本。将字段定义为volatile的,可以阻止这些编译器优化。从而直接从内存进行读取,而不被缓存。一旦在该字段上发生了写操作,所有任务中的所有读操作都会看到(该写操作产生的)变更。如果volatile变量恰好存在于本地缓存中,它就会被立即写入主存,对该字段的所有读操作都会一直发生在主存上。

在以下情况中,应该把变量定义为volatile的

  1. 该变量会同时被多个任务访问
  2. 这些访问中至少有一个是写操作
  3. 试图避免同步

特殊情况:可以让多个线程对该变量进行写操作,只要他们不用先读取该变量,再用读出来的值生成新的值写回该变量。

例如 i++,这种情况会让这个变量失去同步的作用。因为自增操作分为两步,任何一步都可能被中断而成为断点。

如果那个变量受synchronized方法或者语句块的保护,或者是Atomic类型,那就不需要把它设为volatile的。

6.4.3 (指令)重排序和先行发生

public class ReOrdering implements Runnable {
    int one, two, three, four, five, six;
    volatile int volaTile;

    @Override
    public void run() {
        one = 1;
        two = 2;
        three = 3;
        volaTile = 92;
        int x = four;
        int y = five;
        int z = six;
    }
}

one two three 的赋值操作可能会被重新排序,只要他们都发生在volatile的写操作之前。

同样,x y z 也可能会被重排序,只要他们都发生在volatile写操作之后。

volatile操作通常被称为内存栅栏,先行发生确保了volatile变量的读写指令无法穿过内存栅栏而被重排序。

先行发生保证还有另一种效果:当一个线程对某个volatile变量执行写操作时,所有在该写操作之前被该线程修改的其他变量——包括非volatile变量,也都会被刷新到主存。当一个线程读取volatile变量的时候,也会读取到所有和volatile变量一起被刷到主存的变量,包括非volatile变量。

虽然这个特性很重要,解决了Java5之前很隐蔽的一些BUG,但是你不应该依赖该特性,“自动”将周围的变量隐式的变成了volatile的。

如果你希望某个变量是volatile的,就应该让其他所有的代码维护人员明确的知道。

6.5 原子性

public class UnsafeReturn{
    
    private int i = 0;

    public int getInt(){
        return i;
    }
    
    @Override
    public synchronized void evenIncrement(){
        i++;
        i++;
    }
}

你很容易地会认为调用getInt方法会很安全,但实际上不是,虽然return i确实是原子操作,但这里并未加上同步。这会导致该值能在对象处于不稳定的中间态时被读取到。此外,i也不是volatile的,因此会存在可见性的问题。

因此,在i未设置为volatile的情况下,getValue和evenIncrement都必须是synchronized的。

Java的自增操作并不是原子的。

6.5.2 原子类

Java5引入了一些特殊的原子变量类,如AtomicInteger,AtomicLong,AtomicReference等。

这些类提供了原子性的更新能力,充分利用了现代处理器的硬件级别原子性,实现了快速无锁的操作。

6.6 临界区

有时候,我们只是想放置多个线程同时访问方法中的部分代码,而不是整个方法。

要隔离的代码区域,被称为临界区,它也是由synchronized来创建的。但是用的语法不同。

synchronized(Object){
    something...
}

这也叫做同步控制块

必须先获得Object的锁,才能进入这段代码

其他没有获得锁的任务在外等待。

记住,使用同步控制块同样有危险:他要求你必须能确定控制块外部的非synchronized代码的确是安全的。

6.6.1 在其它对象上进行同步

synchronized控制块必须在某个对象上进行同步。

通常最合理的就是通过synchronized(this)使用当前对象,这也是普遍的方法。

通过这种方式,在获得这个控制块后,便无法调用同一对象中的其他synchronized方法和临界区了。

有时候必须在另一个对象上进行同步,但如果必须这么做,那就必须确保所有的相关任务都在同一个对象上进行同步。

总结:一个对象里面只有一把锁,被一个线程获得后他就不再拥有这把锁了,线程执行完后返还这把锁。

6.6.2 使用显式的Lock对象

如果在使用synchronize关键字时出现了什么问题,便会抛出异常,但是并没有机会执行任何清理操作,以将系统维持在正常的状态。而如果使用显式的Lock操作,便可以用try-catch-finally语句来维护

6.7 库组件

java.util.concurrent库提供了大量用于解决并发问题的类,可以帮助你实现更简单,更稳健的并发程序。

不过请注意,相比CompletableFuture和并行流,这些工具属于更底层的机制。

  1. 延迟队列 DelayQueue:这是一种实现了Delay接口的对象组成的无边界BlockingQueue。
    一个对象只有在其延迟时间到期后才能从队列取出。
  2. 优先级阻塞队列 PriorityBolckQueue:这基本上是一种可以阻塞读取操作的优先队列。

6.8 总结

本章主要是为了让你遇到底层并发代码的时候,能够对其有一定的了解,尽管这对该主题还远谈不上全面。

下面是并发编程需要遵循的步骤

  1. 不要使用并发,想办法找别的方法来使程序运行得更快。
  2. 如果必须使用并发,就使用最新的高级工具:CompletableFuture和并行流。
  3. 不要在任务间共享变量。任何必须在任务间传递的信息都应该通过java.util.concurrent库中的并发数据结构来共享。
  4. 如果必须共享变量,要么用java.util.concurrent.atomic类型,要么对任何可以间接访问直接访问这些变量的方法应用为synchronized
posted @ 2022-09-27 07:23  Y&Qter  阅读(60)  评论(0编辑  收藏  举报