《Java核心技术卷I》读书笔记

2020年7月8日 1~3章

  1. Java大小写敏感强类型(必须为每个变量声明一种类型)。
  2. 类名采用骆驼命名法(CamelCase),常量名全大写
  3. byte short int long float double char boolean 2 4 8 16 4 8 字节 一个字节等于8位。·
  4. Java7后可加0b(0B)前缀写二进制数,还可为数字字面量加下划线(更易读)。
  5. float类型后有f(F),没有或有d(D)的视为double类型。
  6. 浮点数值不适用于无法接受舍入误差的金融计算

如2.0-1.1将返回0.8999999而非0.9,这是由于浮点数值采用二进制系统表示,而在二进制系统中无法精确地表示分数1/10,就像十进制无法精确表示分数1/3一样。

  1. 针对变量的取值只在一个有限的集合内的情况,可自定义枚举类型
  2. &&和||会短路,&和|不会。
  3. 为什么字符串是不可变的? final修饰 相同内容的字符串共享字符串池中已有的资源。如果可变,那一个修改后,其他指向它的字符串变量就全都改变了。不安全。
  4. ==和equals 前者判断是否在同一位置上,后者判断内容是否相同
  5. 空串“”和Null 空串是指向一个值为“”的字符串,而null未指向任何串
  6. StringBuilder,前身为StringBuffer,StringBuilder较StringBuffer的效率稍高一些,但StringBuffer允许采用多线程的方式添加或删除字符,而StringBuilder不支持并发操作。
  7. 注解是为编译器或处理Java源文件或类文件的工具提供信息的一种机制
  8. case支持char,byte,short,int,enum,string等类型。
  9. 标签可应用到任何语句,如循环、判断、块语句。
  10. BigInteger,BigDecimal可处理任意长度数字序列的数值。几个常用方法:valueof(),add(),subtract(),multiply(),divide(),mod(),sqrt()(9引入)。
  11. 长度为0的数组与null不相同。
  12. 创建数字数组时,所有元素初始化为0,boolean数组初始化为false,对象数组初始化为null。
  13. 数组拷贝:Arrays.copyof()。

2020年7月9日 第4章 对象与类

  1. 实现封装的关键在于,绝对不能让类中的方法直接访问其他类的实例字段。因此,一个类的实例字段最好全部为private。
  2. 在Java中,任何对象变量的值都是对存储在另一个地方的某个对象的引用。new操作符返回的也是一个引用。
  3. 在Java中,所有对象都存储在堆中。当一个对象包含另一个对象变量时,它只是包含着另一个堆对象的指针。而如果想获得对象的完整副本,必须使用clone()。
  4. 更改器方法访问器方法(是否只访问对象而不修改对象),字段访问器。
  5. 构造器
    ①构造器与类同名,且每个类可以有一个以上的构造器(重载
    ②构造器可以有0,1,或多个参数,且没有返回值
    ③构造器总是和new操作符一起调用
    ④当一个类没有编写构造器时,会自动提供一个无参数构造器。无参数构造器,将所有实例字段设为默认值。但如果类中提供了至少一个构造器,但没有无参数构造器,那么构造对象时如果不提供参数就是不合法的。
    ⑤如果构造器的第一个语句形如this(…),这个构造器将调用同一个类的另一个构造器。
  6. 在Java 10中,如果可从变量的初始值推导出其类型,那么可以用var关键字声明局部变量,而无须指定类型。注意var关键字只能用于方法中的局部变量。参数和字段的类型都必须声明。
  7. 处理 null 的 宽容型方法 Object.requireNonNullElse(n,“unknown”) 和 严格型方法 Object.requireNonNull(n,"The name cannot be null) (on Java 9).
  8. 隐式参数(方法调用的目标或接受者)和显式参数(方法名后括号中的数值),方法中的this关键字指示的是隐式参数。
  9. final修饰基本类型时,其值不可改变,修饰对象类型时,其引用不可改变。原生方法(非Java实现),可绕过Java语言的访问控制机制,对final修饰的变(常)量进行修改。
  10. 静态
    ①static修饰的字段即静态字段,又称类字段,为该类的所有实例所共享
    ②静态方法是不在对象上执行的方法,可认为其没有this参数。(其实可以用对象调用静态方法,但这样容易产生混淆)(常使用静态工厂方法来构造对象)
    ③方法不需要访问对象状态或只需要访问类的静态字段时,可使用静态方法。
  11. 按值调用 call by value表示方法接收的是调用者提供的值。而按引用调用 call by reference表示方法接收的是调用者提供的变量地址。方法可以修改按引用传递的变量的值,而不能修改按值传递的变量的值。
  12. Java总是采用按值调用,即方法得到的是所有参数值的一个副本。具体来讲,方法不能修改传递给它的任何参数变量的内容。在Java中,方法不能修改基本数据类型的参数,可以改变对象参数的状态,但不能让一个对象参数引用一个新的对象。
    即想交换两对象参数的引用这种事是做不到的
    原因:传进方法的对象参数只是原参数的副本,对对象参数进行修改引用的操作不会影响原参数,在方法结束后对象参数就消失了,对结果构不成任何影响
  13. Java允许重载任何方法,查找匹配方法签名(方法名,参数数量,参数类型,不包含返回类型)的过程被称为重载解析
  14. 如果在构造器中没有显式地为字段设置初值,就会被自动赋为默认值(0,false,null)。但方法中的局部变量必须明确地初始化。
  15. 除了在构造器中设置值在声明时赋值,初始化数据字段的方法还有初始化块
  16. 调用构造器的具体处理步骤
    ①如果构造器的第一行调用了另一个构造器,则基于所提供的参数执行第二个构造器。
    ②否则,
    a) 所有数据字段初始化为其默认值(0,false,null)
    b) 按照在类声明中出现的顺序,执行所有字段初始化方法和初始化块。
    ③执行构造器主体代码。
  17. 静态字段可由提供初始值,使用静态初始化块等方法初始化。当类第一次加载的时候,将会进行静态字段的初始化。与实例字段一样,除非将静态字段显式的设置成其他值,否则默认的初始值是0,false,null。所有的静态字段初始化方法以及静态初始化块都将依照类声明中出现的顺序执行。
  18. 静态导入可导入静态方法和静态字段,而非仅是类。
  19. 类注释,方法注释,字段注释,通用注释,包注释
  20. 类设计技巧
    ①保证数据私有
    ②保证数据被初始化
    ③不要在类中使用过多的基本类型,应活用其他的类替换多个相关的基本类型
    ④不是所有的字段都需要单独的字段访问器和字段更改器
    ⑤分解有过多职责的类
    ⑥类名和方法名要能够体现它们的职责
    ⑦优先使用不可变的类(如果类是不可变的,就可以安全地在多个线程间共享其对象)

2020年7月10日 第5章 继承

  1. super,this 的对比
    ①区别:super只是一个指示编译器调用超类方法的特殊关键字,而this关键字代表的是一个对象引用
    ②含义:关键字this有两个含义,一是指示隐式参数的引用,二是调用该类的其它构造器。而super关键字的两个含义分别是,一调用超类的方法,二调用超类的构造器。(调用构造器的语句只能作为另一个构造器的第一条语句出现)
  2. 重载(同一类中同名函数间),覆盖(子类和父类签名相同的方法间)
  3. 覆盖方法时,需要保证返回类型的兼容性。(子类将覆盖方法的返回类型改为原返回类型的子类型是允许的,这称这两个方法有可协变的返回类型
  4. 同一个对象变量可以指示多种实际类型的现象称为多态(注意,不能将超类的引用赋给子类变量)。
  5. 在运行时能够自动地选择适当的方法,称为动态绑定。动态绑定有一个非常重要的特性:无须对现有的代码进行修改就可以对程序进行扩展。
  6. 静态绑定是指,当方法被private,static,final修饰或方法是构造器时,编译器可以准确地知道应该调用哪个方法。
  7. 替换原则(用来判断是否应将数据设计为继承关系):程序中出现的超类对象的任何地方都应该可以使用子类对象替换。
  8. 调用方法的过程
    ①虚拟机获取隐式参数的实际类型的方法表
    ②虚拟机查找定义了方法签名的类
    ③虚拟机调用这个方法
  9. 被final修饰的类不允许扩展(其所有方法均自动被final修饰),被final修饰的方法不允许被覆盖。
  10. 内联:如果一个方法没有被覆盖并且很短,编译器就能够对它进行优化处理,这个过程称为内联
  11. 强制类型转换只能在继承层次内进行。在将超类强制转换成子类之前,应该使用instanceof进行检查。
  12. 除了抽象方法之外,抽象类还可以包含字段和具体方法。
  13. 扩展抽象类可以实现部分抽象方法,使子类仍为抽象类,也可实现全部抽象方法,使子类成为普通类。
  14. 抽象类不可被实例化。抽象类的对象变量可以定义,但只能引用非抽象子类的对象。
  15. public:公开,任何类均可访问;默认、包访问权限:同一包内的类均可访问;protected:本包及其所有子类可以访问;private:只有其本身可以访问。
  16. Object类实现的equals方法将确定两个对象引用是否相等(与==用于对象时一致)。
  17. equals方法应该具有的特性
    自反性:对于任何非null的x,x.equals(x)应该返回为true
    对称性:对于任何非Null的x和y,当且仅当y.equals(x)返回true时,x.equals(y)也应该返回true
    传递性:对于任何引用x、y、z,如果x.equals(y)返回为true,y.equals(z)返回也为true,则x.equals(z)也返回为true
    一致性:如果x和y引用的对象没有发生变化,反复调用x.equals(y)返回的结果应该是相同的
    非空性:对于任何非null的x,x.equals(null)应该返回为false
  18. 下面给出编写一个完美的equals方法的建议:
    ①显示参数类型为Object,命名为otherObject,稍后需要将它强制转换成另一个名为other的变量
    ②检测this与otherObject(的引用)是否相等:if(this == otherObject) return true;
    ③检测otherObject是否为null,如果是则返回false:if(otherObject ==null) return false;
    ④比较this与otherObject的类。如果equals的语义可以在于类中改变,就使用getCIass检测
    if (getClass() = otherObject.getClass()) return false;
    如果所有的子类都有相同的相等性语义,可以使用instanceof检测:
    if ((otherObject instanceof ClassName)) return false;
    ⑤.将otherobject强制转换为相应类类型的变量:
    ClassName other = (ClassName) otherobject
    ⑥现在根据相等性概念的要求来比较字段。使用= =比较基本类型字段,使用Object.equals比较对象字段。如果所有的字段都匹配,就返回true;否则返回false;
    return field1 ==other.field1&& Objects.equals(field2, other.field2)&&…
    如果在子类中重新定义eqals,就要在其中包含一个super.equals(other)调用。

提示,对于数组类型的字段,可以使用静态的Arrays.equals()方法检测相应的数组元素是否相等

  1. Object默认的hashcode方法会从对象的存储地址得出散列码,而String类的散列码由内容导出的。
  2. 如果重新定义了equals方法,就必须重新定义hashcode方法。即:equals和hashcode必须相容:如果x.equals(y)返回true,则x.hashCode()就必须和y.hashCode()返回相同的值。
  3. 只要对象与一个字符串通过操作符“+”连接起来,Java编译器就会自动地调用toString()来获得该对象的字符串描述。
  4. Object类定义的toString方法可以打印出对象的类名和散列码。而要想打印数组可调用Arrays.toString()和Arrays.deepToString()。
  5. 菱形语法:没有使用var关键字的时候,可以省去右边泛型类的类型参数。
  6. 数组列表(即ArrayList)的容量与数组的大小有一个非常重要的区别:如果分配一个有100个元素的数组,数组就有100个空位置(槽)可以使用。而容量为100个元素的数组列表只是可能保存100个元素(实际上也可以超过100,不过要以重新分配空间为代价,但是在最初,甚至完成初始化构造之后,数组列表不包含任何元素
  7. 数组列表转换成数组的方法:对数组列表对象使用toArray(a)(a为数组对象)
  8. 类型擦除:编译器未检查出违反规则的现象后,就将所有的类型化数组列表转换成原始ArrayList对象。在程序运行时,所有的数组列表都是一样的,即虚拟机中没有类型参数
  9. 自动装箱自动拆箱编译器要做的工作,而非虚拟机。编译器在生成类的字节码时会插入必要的方法调用,虚拟机只是执行这些字节码。自动装箱
  10. 变参方法
  11. 枚举类不可能构造新的对象,因其构造器是私有的。
  12. 异常有两种类型,非检查型异常检查型异常
    非检查型异常:数组越界,空指针
    检查型异常:IO异常,反射操作异常(ReflectIveOperationException)
  13. 继承设计的技巧
    ①将公共操作和字段放在超类中
    ②不要使用受保护的字段
    ③使用继承实现“is-a”关系
    ④除非所有继承的方法都有意义,否则不要使用继承
    ⑤在覆盖方法时,不要改变预期的行为
    ⑥使用多态,而不要使用类型信息
    ⑦不要滥用反射
  14. 反射相关知识
  15. 反射相关的应用–修改string的值

2020年7月11日 第6章 接口、lambda表达式与内部类

  1. 接口不是类,不能用new 实例化接口。但可以声明接口变量,其必须引用实现了这个接口的类对象。
  2. 接口中的所有方法都自动是public方法,实现接口时必须把方法声明为public。而在Java 9中,接口中的方法可以是private,private方法可以是静态方法或实例方法。
  3. 接口可以定义常量,但绝不会有实例字段,默认为public static final。
  4. 在Java 8之前,接口中绝不会实现方法
  5. compareTo方法应当与equals方法兼容。

BigDecimal除外,1.0equals(1.00) false compareTo true

  1. 为什么不能在类中直接提供compareTo方法,而一定要实现Comparable接口呢? 因为:Java是强类型语言,在调用方法的时候,编译器要能检查这个方法确实存在。
  2. 如果翻转compareTo的参数,结果的符号也应该翻转(但具体值不一定)。
  3. 接口和抽象类
    ①使用抽象类表示通用属性存在一个严重的问题,即每个类只能扩展一个类,而Java又不支持多重继承。
    什么时候应该使用接口,什么时候应该使用抽象类?答:抽象类相当于一个模板,模板中有子类可以公用的部分,也有需要子类自行实现的部分,是为模板模式设计;而接口是对行为的抽象,它只定义一组行为规范,每个实现类都要实现所有规范,是为辐射式设计
  4. 可以为接口方法提供一个默认实现,用default修饰。
  5. 默认方法的一个重要用法是“接口演化” ,将接口增加的新方法设置为默认方法可以保证“源代码兼容”。
  6. 接口方法实现发生冲突时遵循两个原则,一是超类优先,而是接口冲突。(所以千万不要让一个默认方法重新定义一个Object类中的某个方法,由于超类优先原则,这样的方法是永远无效的)
  7. 回调是一种常见的程序设计模式,在这种模式中,可以指定某个特定事件发生时应该采取的动作。如:按下鼠标或选择某个菜单项时,你可能希望完成某某个特定的动作。
  8. 浅拷贝:两个不用的变量引用指向相同的对象,或者虽然指向不同的对象但其还是有指向相同对象的实例字段存在
  9. 深拷贝:两个不用的变量引用指不同的,且无指向相同对象的实例字段存在的对象
  10. lambda 表达式的基础知识
  11. lambda 表达式的实际应用
  12. 方法引用指示编译器生成一个函数式接口的实例,覆盖这个接口的抽象方法来调用给定的方法,如System.out::println。
  13. 类似于lambda表达式,方法引用也不是一个对象。不过,为一个类型的为函数式接口的变量赋值时会生成一个变量。
  14. 方法引用就是lambda表达式的一种简写
  15. 可以在方法引用中使用this参数,例如,this::equals等同于x->this.equals(x)。使用super也是合法的(super::instanceMethod)。
  16. lambda表达式有三个部分:一个代码块,参数,自由变量的值(指非参数而且不在代码中定义的变量)
  17. 在lambda表达式中,只能引用值不会改变的变量。例如,下面的做法是不合法的:
	public static void countDown(int start, int delay) {
        ActionListener listener = event -> {
            start--;//ERROR:无法改变被捕获的值
            System.out.println(start);
        };
        new Timer(delay,listener).start();
    }
  1. 如果在lambda表达式中引用一个变量,而这个变量可能在外部改变,这也是不合法的。
    public static void repeat(String text, int count) {
        for (int i = 0; i <=count ; i++) {
            ActionListener listener = e -> {
                System.out.println(i+":"+text);
                //ERROR:无法改变i
            }
        }
    }
  1. 综上,lambda表达式中捕获的变量必须实际上是事实最终变量(effectively final),即:这个变量初始化之后就不会再为它赋新值。(当然,final也可)
  2. lambda表达式的体与嵌套块有相同的作用域,这里同样适用命名冲突和遮蔽有关规定。
  3. 使用lambda表达式的重点是延迟执行。毕竟,如果想要立即执行代码,完全可以直接执行,而无须把它包装在一个lambda表达式中。之所以希望以后再执行代码,这有很多原因,如:
    ● 在一个单独的线程中运行代码;
    ● 多次运行代码;
    ● 在算法的适当位置运行代码(例如,排序中的比较操作)
    ● 发生某种情况时执行代码(如,点击了一个按钮。数据到达,等等)
    ● 只在必要时才运行代码:
  4. 内部类 inner class是定义在另一个类中的类,使用它有两大原因:
    1.内部类可以对同一个包中的其他类隐藏
    2.内部类方法可以访问定义这个类的作用域中的数据,包括private数据
  5. 内部类原先对于简洁的实现回调相当重要,但如今lambda表达式在这方面可以做的更好。
  6. 内部类的对象有一个隐式引用,指向实例化这个对象的外部类对象。通过这个指针,它可以访问外部对象的全部状态。而静态内部类没有这个指针。
  7. 内部类是一个编译器现象。
  8. 局部内部类
    ①是定义在方法中的内部类,定义它时不能有访问说明符,其作用域被限定在声明这个局部类的块中。
    ②它可以对外部世界完全隐藏。
    ③它不仅能够访问外部类的字段,还可以访问局部变量(但必须是事实最终变量)。
  9. 匿名内部类
    ①只创建这个类的一个对象,甚至不需为类指定名字
    ②匿名内部类没有构造器,原因是构造器的名字必须与它的名字相同,而它没有名字。但可以提供一个对象初始化块。
    ③之前用来实现事件监听器和其他回调,如今最后还是使用lambda表达式。
    ④双括号初始化:下面的技巧称为“双括号初始化”,这里利用了内部
    类语法。假设你想构造一个数组列表,并将它传递到一个方法:,
	var friends- new ArrayList<String>();
	friends.add("Harry");
	friends.add("Tony");
	invite(friends);

如果不再需要这个数组列表,最好让它作为一个匿名列表。不过作为一个匿名列表,该如何为它添加元素呢?方法如下:

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

注意这里的双括号。外层括号建立了ArrayList的一个匿名子类。内层括号则是一个对象初始化块(见第4章)。
在实际中,这个技巧很少使用。大多数情况下,invite方法会接受任何
List,所以可以直接传入List.of(Harry",“Tony”)。

  1. 静态内部类
    ①对于只是为了把一个类隐藏在另外一个类的内部,并不需要内部类有外围类对象的引用的这种类,可将其声明为静态内部类。
  2. 服务加载器和代理暂且略过。

2020年7月12日 第7章 异常、断言和日志

  1. 异常处理的任务就是将控制权从产生错误的地方转移到能够处理这种情况的错误处理器。
  2. 在Java中,有三种处理系统错误的机制:抛出异常日志断言
  3. 在Java中,异常对象都是派生于Throwable类的一个类实例。异常分为两类:ErrorException
  4. Error类层次结构描述了Java运行时系统的内部错误资源耗尽错误
  5. Exception分为两种:RuntimeException其他异常。前者属于由编程错误导致的异常,如:数组越界异常空指针异常强制类型转换异常。后者属于程序本身没有问题,但由于像IO错误这类问题导致的异常属于其他异常IOException),如:文件不存在异常
  6. ErrorRuntimeException又被称为非检查型异常,所有其他的异常IOException)被称为检查型异常
  7. 检查型异常需要被声明,即抛出(throw)。
  8. 一个方法必须声明所有可能抛出的检查型异常,而非检查型异常要么在程序员的控制之外(Error),要么是由从一开始就应该避免的情况所导致的(RuntimeException)
  9. 子类抛出的异常不能比父类更通用,父类未抛出异常时子类也不能抛出异常(但可以处理异常),也不允许在 子类方法中抛出超类方法未抛出的异常。
  10. 捕获那些知道如何处理的异常,而继续传播那些你不知道怎样处理的异常。
  11. catch字句可以合并,可以使代码看起来更简单,还会更高效。
try{
...
}
catch{ FileNotFouneException | UnknownHostException e){
...
}
catch( IOException e){
...
}
  1. 如果希望改变异常的类型,可以在catch子句中将原始异常设置为新异常的“原因”( e.initCause(orginal) ),然后再抛出新异常。当捕获到这个异常时,可以用getCause()获得原始异常。
  2. 如果一个方法发生了一个检查型异常,但方法并不允许抛出检查型异常,就可以使用这一包装技术,将检查型异常捕获并包装成一个运行时异常
  3. 警告:当finally子句包含return语句时, 有可能产生意想不到的结果。假设利用return语句从try语句块中间退出。在方法返回前,会执行finally子句块。此时,如果finally中的也有一个return语句,这个返回值将会遮蔽原来的返回值。来看下面这个例子:
putlic static int parseInt(String s){
	try{
		return Integer .parseInt(s);
	}	
	finally{
		return 0; // ERROR
	}	
}

看起来在parseInt(“42”)调用中,try块的体会返回整数42。不过,这个方法真正返回之前,会执行finally子句,这就使得方法最后会返回0,而忽略原先的返回值。
而更糟糕的是,考虑调用parseInt(“zero”)。Integer.parseInt方法会抛出一个Number.FormatException。然后执行finally子句,return 语句甚至会“吞掉”这个异常!
所以,finally子句要用于清理资源。不要把改变控制流的语句( return,throw,break,continue)放在finally子句中。

  1. try-with-Resources语句(带资源的try语句),当try块退出时会自动调用close()方法关闭资源
try (Resource res = ... ){
	work with res
}

下面给出一个典型的例子,这里要读取个文件中的所有单词:

try (var in = new Scanner(
new FileInputStream("/usr/share/dict/words"),StandardCharsets.UTF_8)){
	while (in.hasNext(){
	System.out.println(in.next());
	}
}

这个块正常退出时,或者存在个异常时, 都会调用in.close方法,就好像使用了finally块一样。

  1. 在Java9中,可以在try首部中提供之前声明的事实最终变量
public static void printAll(String[] lines, Pintriter out){
	try (out) { // efectively final variable
		for (String Line : Lines)
		out. printLn(line);
	}// out.close() called here
} 
  1. 如果try块抛出一个异常,而且close方法也抛出一个异常, 这就会带来一个难题。try-with-resources语句可以很好地处理这种情况。原来的异常会重新抛出,而close方法抛出的异常会“被抑制”。这些异常将自动捕获。并由addSuppressed方法增加到原来的异常。如果对这些异常感兴趣,可以调用getSuppressed方法,它会生成从close方法抛出冰杯抑制的异常数组。
  2. 只要需要关闭资源,就要尽可能使用try-with-resources语句。
  3. 注释: try-with-resources语句自身也可以有catch子句,甚至还可以有一个finally语句。这些子句会在关闭资源之后执行
  4. 堆栈轨迹是程序执行过程中某个特定点上所有挂起的方法调用的一个列表。
  5. 断言:Java语言引人了关键学assert这个关键字。有两种形式: assert condition;assert condition : expression;。这两个语句都会计算条件,如果结果为false,则抛出一个AssertionError异常.在第二个语句中,表达式将传入AssertionError对象的构造器,并转换成个消息字符串。
  6. “表达式” (expression) 部分的唯一目的是产生一 个消息字符串。
  7. 断言失败是致命的、不可恢复的错误,所以断言检查只能在开发和测试阶段进行
  8. 日志部分暂且不谈,原书给的是java默认日志系统,安卓开发中使用的不是这种。
  9. 调试技巧:①sout ②每个类放置单独的main方法进行单元测试 ③JUnit ④日志代理⑤Throwable类的printStackTrace方法

2020年7月13日 第8章 泛型程序设计

  1. 泛型程序设计意味着代码可以对多种不同类型的对象重用
  2. 泛型类就是有一个或多个类型变量的类。
  3. 类型变量通常由简短的大写字母表示。在Java库中,E表示集合的元素类型,K和V分别表示表的键和值的类型。T(U,S)表示任意类型。
  4. 泛型方法中,类型变量放在修饰符后,返回类型之前
  5. 泛型方法既可以在泛型类中定义,也可以在普通类中定义。
  6. 虚拟机中没有泛型类型对象——所有对象都属于普通类
  7. 无论何时定义一个泛型类型,都会自动提供个相应的原始类型(raw type这个原始类型的名字就是去掉类型参数后的泛型类型名。类型变量会被擦除(erased), 并替换为其限定类型(或者,对于无限定的变量则替换为Object).
  8. 原始类型用第一个限定来替换类型变量,或者,如果没有给定限定,就替换为Object。而为了提高效率,我们通常把标签接口(即没没有方法的接口)放在限定列表的末尾
  9. 类型擦除可能会带来一个问题——可能会和多态产生冲突。为了解决这一问题,编译器会生成一个桥方法,调节两种方法的矛盾。
  10. 对于Java泛型的转换,需要记住以下几个事实:
    虚拟机中没有泛型,只有普通的类和方法
    所有的类型参数都会替换为它们的限定类型
    ③编译器会合成桥方法来保持多态。
    ④为保持类型安全性,必要时编译器会插入强制类型转换
  11. 使用Java泛型时需要考虑的一些限制:
    ①不能用基本类型实例化类型参数
    运行时类型查询只能查到原始类型。如对Pair< String > a进行getClass方法,只能得到Pair类的类型信息。
    不能创建参数化类型的数组由于类型擦除,只能构造出Object[],在使用时如果存储其他类型的元素,就会抛出ArrayStoreException异常。
    ④Varargs警告为包含调用该警告的方法可以用@SuppressWarnings(“unchecked”)注解抑制,也可以用@SaveVarargs直接注解该方法。但后者只能用于声明为static、final或(在Java 9中)private的构造器和方法,其他方法都有可能被覆盖,使得这个注解没什么意义。
    不能实例化类型变量。因为类型擦除,你将实例出一个Object
    ⑥不能构造泛型数组。
    ⑦泛型类的静态上下文中类型变量无效。
    ⑧不能抛出或捕获泛型类的实例。catch子句中不能使用类型变量
    ⑨可以取消对检查型异常的检查。
    ⑩注意擦除后的冲突。为了支持类型擦除,我们要是施加一个限制:倘若两个接口类型是同一接口的不同参数化,一个类或类型变量就不能同时作为这两个接口类型的子类。
  12. 无论S和T有什么关系,Pair< S > 和Pair< T >都没有任何关系
  13. 在通配符类型中,允许类型参数发生变化。子类型限定:Pair<? extends Emploee>。超类型限定:Pair<?super Manager>。
  14. 带有超类型限定的通配符允许我们写入一个泛型对象,而带有子类型限定的通配符允许我们读取一个泛型对象。具体可见:java泛型程序设计——通配符类型+通配符的超类型限定
  15. 还可以使用根本无限定的通配符。**Pair<?>和Pair的本质不同在于:可以用任意的Object对象调用原始Pair类的setFirst方法。**但要记住,通配符不是类型变量,不能在编写代码中使用“?"作为一种类型。?可以被用来捕获。java泛型程序设计——无限定通配符+通配符捕获

2020年7月14日 第9章 集合

  1. Java集合类库采用接口与实现分离
  2. 循环数组比链表更高效,但它是有界集合,容量有限。当要收集的对象没有上限时,就得使用链表。
Queue<T> queue = new LinkedList<>();
//链表实现的队列
Queue<T> queue = new ArrayDeque<>();
//循环数组实现的队列
  1. 编译器将“for each”循环转换为带有迭代器的循环。也可以不写循环,而是调用forEachRemaining方法并提供一个lambda表达式,将会对迭代器的每一个元素调用这个lambda表达式。
  2. 访问元素的顺序取决于集合的类型。
  3. 可以认为Java迭代器在两个元素之间。当调用next时,迭代器就越过下一个元素,并返回刚刚越过的那个元素的引用。
  4. 迭代器的remove方法将会删除上次调用next方法时返回的元素。如果调用remove方法之前没有调用next方法,将是不合法的,会抛出异常。
  5. 在这里插入图片描述
  6. Java中的链表实际上都是双向链表
  7. 多次调用add方法,将按照提供的次序把元素添加的链表中。在这里插入图片描述
  8. 如果一个迭代器发现它的集合被另一个迭代器修改了,或是被该集合自身的某个方法修改了,就会抛出并发修改异常。(链表只跟踪对列表的结构性修改,如添加和删除链接,修改(set)方法不被视为结构性修改。
  9. 链表不支持快速随机访问,数组支持;链表可以高效的在列表中间添加或删除元素,数组不可以。
  10. 数组列表 ArrayList 封装了一个动态再分配的对象数组。
  11. Vector类的所有方法都是同步的,即并发安全的。而ArrayList的方法不是同步的。
  12. 散列表可用于快速查找对象。
  13. Java中,散列表用链表数组实现,数组的每个链表被称为一个桶。要想查找表中对象的位置,就先计算散列码,然后与桶的总数取余,所得到的结果就是保存这个元素的桶索引
  14. 当出现散列冲突时,有两种解决办法:
    链表法,直接加在链表后面(Java中就是这样实现的)
    开放寻址法,又分为线性探测法,二次探测法,双重散列法
  15. 在Java 8 中,桶满时会从链表变为平衡二叉树
  16. 桶数是指用于收集有不同散列值的桶的数目。
  17. 标准类库采用的桶数是2的幂,默认值为16。
  18. 如果散列表太满,就需要再散列装填因子(默认为0.75,综合了时间空间效率考虑)可以确定何时对散列表进行散列。新表的桶数是原来的两倍。旧表中的元素全部被插入新表后,旧表将被丢弃。
  19. 集中的元素散列码如果发生了改变,元素在数据结构中的位置也会发生变化。
  20. 树集 TreeSet 是一个有序集合,任意顺序插入,遍历时自动安装排序后的顺序呈现。排序依靠红黑树实现。
  21. 队列与双端队列 Queue Dueue add,remove,等常规方法与offer,poll等方法区别,前者出现特殊情况如队列满、空时抛出异常,后者返回null。
  22. 优先队列 PriorityQueue 用堆实现,任意顺序插入,按照有序顺序检索。
  23. 映射 Map 散列映射(HashMap)稍快一些,如果不需要有序地访问键(使用TreeMap),最后选择散列映射。
  24. 如果映射中没有存储与给定键对应的信息,get将返回Null。
  25. getOrDefault方法: 如
map.getOrDefault(id,0); //gets 0 if id is not present
  1. putIfAbsent方法:只有当键不存在或映射到null时才放入一个值。
  2. merge方法:如果键原先不存在,下面的调用将把word和1关联,否则使用Integer::sum函数组合原值和1,即将原值与1求和。
map.merge(word, 1, Integer::sum);
  1. 视图 view 实现了Collection接口或某个子接口的对象。
  2. 视图有三种,键集 keySet(既非HashSet也非TreeSet,而是实现了Set接口的另外某个类的对象),值集合 values(不是一个集,可重复,键值对集 entrySet
  3. 如果在键集视图上调用迭代器的remove方法,实际上会从映射中删除这个键和与它关联的值。不支持add方法。
  4. 弱散列映射 WeakHashMap 当对键(用弱引用保存)的唯一引用来自于散列表映射条目时,该数据结构将与垃圾回收器协同工作,一起删除键值对。
  5. 链接散列集与映射 LinkedHashSet LinkedHashMap 会记住插入元素项的顺序。
  6. 最近最少使用原则。
  7. 枚举集 EnumSet 是一个键类型为枚举类型的映射。
  8. 标志散列映射IdentityHashMap 使用Object.hashcode 根据对象的内存地址计算散列值。
  9. Java 9引入了一些静态方法(of, ofEntries等,可以生成给定元素的集或列表,以及给定键值对的映射(不能为null)。这些集合是不可修改的。如果需要一个可更改的,则可把这个不可修改的集合传递到构造器。
  10. subList(左闭右开)方法可用来获得列表子范围的视图。可以对子范围应用任何操作,而且操作会自动反应到整个列表。
  11. 对于有序集和映射,from to可返回大于等于from且小于to的所有元素构成的子集。
  12. 检查型视图可以探测将错误类型元素混入泛型集合中的问题,会在正确的位置报告错误。
  13. 差1错误
  14. 二分查找只能用于支持随机访问,且有序的集合。
  15. 批操作:
    a.removeAll(b) 从a中删除所有b中出现的元素
    a.retainAll(b) 从a中删除所有b中没有出现的元素 (可用来求交集)
  16. 集合与数组的转换
    如果需要把一个数组转换为集合,List.of 包装器可以达到这个目的。例如:
String[] values= . . .  ;
var staff = new HashSet<>(List.of(values));

从集合得到数组会更困难一些。当然,可以使用toArray 方法:

Object[] values = staff. toArray();

不过,这样做的结果是一个对象数组。尽管你知道集合中包含的是一个特定类型的对象,但不能使用强制类型转换:

String[] values = (String[]) staff. toArray(); // 将抛出类型转换异常

toArray方法返回的数组创建为一个Object[]数组,不能改变它的类型。实际上,必须使用toArray方法的一个变体,提供一个指定类型而且长度为0的数组。这样一来,返回的数组就会创建为相同的数组类型:

String[] values = staff. toArray(new String[0]);

如果愿意,可以构造一个大小正确的数组:

staff.toArray(nev Sting[staff.size()]);

在这种情况下,不会创建新数组。

  1. 将集合接口作为方法参数和返回类型是个很好的想法。
  2. 遗留的集合:
    ①HashTable与HashMap作用一样,不过前者的方法是同步的。现在如果没有对同步的需求可使用HashMap,如果有,应该使用ConCurrentHashMap。
  3. 枚举接口 hasMoreElements 和 nextElement 类似于Iterator接口的hasNext 和 next 方法。区别:Iterator接口多了一个remove。
  4. 属性映射,用于指定程序的配置选项。
  5. Stack 扩展自Vector类,现在普遍使用LinkedList实现的。
  6. 位集用于存储一个位序列。

2020年7月15日 第12章 并发

  1. 进程和线程的主要区别(总结)
  2. 建立线程的两种方式:
    实现Runnable接口,用该Runnable构造一个Thread对象,调用start方法启动线程。
    扩展Thread类,重写run方法 ,构造子类对象,调用start方法(注意,如果直接调用run方法会在当前线程中执行这个任务,而不会启动新线程)。
	Runnable task1 = () ->
      {
         try
         {
            for (int i = 0; i < STEPS; i++)
            {
               double amount = MAX_AMOUNT * Math.random();
               bank.transfer(0, 1, amount);
               Thread.sleep((int) (DELAY * Math.random()));
            }
         }
         catch (InterruptedException ignored)
         {
         }
      };
      new Thread(task1).start();
  1. 线程有以下六种状态:New(新建)、Runnable(可运行)、Blocked(阻塞)、Waiting(等待)、Timed waiting(计时等待)、Terminated(终止)
    当用new操作符创建一个新线程时,线程还没有运行,其状态为新建。调用start方法后,线程就处于可运行状态。
  2. 抢占式调度:系统给每一个可运行线程一个时间片来执行任务,当时间片用完时,系统剥夺线程的运行权,并给另一个线程一个机会来运行。
  3. 协作式调度:一个线程只有在调用yield方法(放弃自身运行权)或者被阻塞或等待时才失去控制权。
  4. 在任何时刻,一个可运行的线程可能正在运行,也可能没有运行(如调用start后,在系统分给线程时间片之前,线程都不会运行)。
  5. 当一个线程试图获取一个内部的对象锁,而这个锁被其他线程占有时,该线程就会进入阻塞状态。当所有其他线程都释放了这个锁,并且线程调度器允许该线程持有这个锁时,它将变成非阻塞状态。
  6. 当线程等待另一个线程通知调度器出现一个条件时, 这个线程会进入等待状态调用Object.wait方法或Thread.join方法(等待指定的线程终止,火等待经过指定的毫秒数),或者是等待java.util.concurrent库中的Lock或Condition时,就会出现这种情况。
  7. 有几个方法有超时参数,调用这些方法会让线程进人计时等待(timed waiting)状态。这一状态将一直保持到超时期满或者接收到适当的通知。带有超时参数的方法有Thread.sleep和计时版的object.wait、Thread.join、 Lock.tryLock 以及Condition.await
  8. 当一个线程阻塞或等待时(或终止时),可以调度另一个线程运行。 当一个线程被重新激活(例如,因为超时期满或成功地获得了一个锁), 调度器检查它是否具有比当前运行线程更高的优先级。如果是这样,调度器会剥夺某个当前运行线程的运行权,选择一个新线程运行。
  9. 线程会由于两种情况进入终止状态:①run方法正常退出,线程自然终止②因为一个没有捕获的异常终止了run方法,使得线程意外终止。
  10. 除了已经被废弃的stop方法,没有办法可以强制线程终止。interrupt方法可以用来请求终止一个线程。该方法被调用后,就会设置线程的中断状态。而如果在对一个被sleep或wait调用阻塞的线程上调用该方法时,那个阻塞调用(sleep或wait调用)将被InterruptedException异常中断。而如果调用interrupt后再调用sleep方法,它不会休眠,会清除中断状态后抛出InterruptedException异常。
  11. 被中断的线程可以决定如何响应中断,是继续执行,还是中断执行。
  12. interrupted方法是一个静态方法,它检查当前线程是否被中断,还会清除该线程的中断状态。而isInterrupted方法是一个实例方法,可以用来检查是否有线程被中断,并不会改变中断状态。
  13. 守护线程:为用户线程提供服务,当只剩下守护线程时,虚拟机就会退出。
  14. 当两个或两个以上的线程需要共享统一数据的存取时,就可能发生竞态条件,即:计算的正确性取决于多个线程的交替执行时序时,就会发生的一种多线程破坏共享数据的情况。为了避免这一情况,就要保证同步存取。
  15. 原子性、可见性、有序性
  16. 悲观锁、乐观锁
  17. 有两种机制可防止并发访问代码块:synchronized关键字ReentrantLock类。前者会自动提供一个锁以及相关的“条件”,后者是一种可重入锁(线程可以反复获得已拥有的锁。锁有一个持有技术来跟踪对lock方法的嵌套调用。线程每使用一次lock后都要调用unlock来释放锁。由于这个特性,被一个锁保护的代码可以调用另一个使用相同锁的方法。)
  18. 使用锁时,不能使用try-with-resources语句,原因有二:首先解锁方法名不是close,其次它的首部希望声明一个新变量,而使用锁的时候我们更可能想使用的是多个线程共享的那个变量。
  19. 公平锁:排队获得(但耗费性能,比常规锁慢很多),非公平锁:尝试加塞,失败后排到队尾
  20. 通常,线程进入临界区后却发现只有满足了某个条件之后它才能执行。可以使用一个条件对象(又成条件变量)来管理那些已经获得了一个锁,却不能做有用工作的线程。同时,还要确保判断条件之后与执行逻辑之间不会被其他线程修改条件变量。这就需要一个锁。
  21. 一个锁对象可以有一个或多个相关联的条件对象,可以用newCondition方法获得一个条件对象。
  22. 只要当线程拥有一个条件的锁时,才能在这个条件上调用await(线程暂停,放弃锁,没有办法自行激活)、singnal(使随机一个等待集中的线程解除其阻塞状态)、signalAll(解除所有线程的阻塞,使这些线程可以在当前线程释放锁之后竞争访问对象)方法。
  23. 锁和条件的要点:
    ①锁用来保护代码片段,一次只能有一个线程执行被保护的代码
    ②锁可以管理视图进入被保护代码段的线程
    ③一个锁可以有一个或多个相关联的条件对象
    ④每个条件对象管理那些已经进入被保护代码段但还不能运行的线程
  24. Object类的wait、notify、notifyAll方法对应了Condition的await、signal、signalAll方法。
  25. 将静态方法声明为同步也是合法的。调用这样的方法时,线程会获得相关类对象的内部锁,其他线程就不再能调用该类的类方法或其他同步静态方法。
  26. 内部锁和条件存在一些限制
    ①不能中断一个正在尝试获得锁的线程
    ②不能指定尝试获得锁时的超时时间
    ③每个锁仅有一个条件可能是不够的。
  27. 除调用同步方法之外,线程还可以通过调用同步块获得锁。
  28. 如果写一个变量,而这个变量接下来可能会被另一个线程读取,或者,如果读一个变量,而这个变量可能已经被另一个线程写入值,那么必须使用同步。
  29. 客户端锁定:使用一个对象的锁来实现额外的原子操作(通过使用一个对象的锁,将非原子操作转化为原子操作)。
  30. 监视器具有的特性:只有私有字段、每个对象都有一个关联的锁、所有方法由锁锁定、锁可以有任意多个相关联的条件。
  31. volatile关键字为实例字段的同步访问提供了一种免锁机制。可为其保证可见性,但无法实现有序性和原子性(不能保证读取、翻转、写入不被中断)。
  32. 还有一种情况可以安全地访问一个共享字段,即这个字段声明为final时。
  33. 死锁:线程间互相拥有对方需要的请求的资源。此时所有线程会被阻塞。
    前提:互斥,请求与保持,不可剥夺,环路等待。
    解决方法:是改变请求资源的顺序。
  34. **为什么废弃了stop和suspend方法?**答:当一个线程终止另一个线程时,无法知道什么时候调用stop方法是安全的,而什么时候回导致对象被破坏。suspend方法挂起一个持有锁的线程后,如果调用suspend方法的线程试图获得同一个锁,程序将进入死锁状态。
  35. 为什么原本线程安全的数据结构会允许非线程安全的操作?有两种情况,其一是多个线程修改一个数据结构,可能会导致其内部结构被破坏而不再可用。这种情况是不会在线程安全的数据结构(如ConcurrentHashMap)发生的。其二是操作序列非原子的,导致结果不可预知。
  36. 任何集合类都可以通过使用同步包装器变成线程安全的。
  37. 线程池中有许多准备运行的线程,这些线程在run方法退出后不会死亡,而会留在线程池中准备为下一个请求提供服务。
  38. Runnable封装一个异步运行的任务,可以将其想象成一个没有参数和返回值的方法,而Callable与其类似,但有返回值。
  39. 执行器类有很多静态工厂方法,用来构造线程池。如图:
    在这里插入图片描述
  40. newCachedThreadPool方法:构造一个维程池,会立即执行各个任务,如果有空闲线程可用,就使用现有空闲线程执行任务;如果没有可用的空闲线程,则创建一个新线程。
  41. newFixedThreadPool方法构造一个具有固定大小的线程池。如果提交的任务数多于空闲线程数,就把未得到服务的任务放到队列中。当其他任务完成以后再运行这些排队的任务。
  42. newSingleThreadExecutor是一个退化了的大小为1的线程池:由一个线程顺序地执行所提交的任务(一个接着一个执行)。
  43. 如果线程生存期很短,或者大量时间都在阻塞,那么可以使用一个缓存线程池。不过,如果线程工作量很大而且并不阻塞,你肯定不希望运行太多线程。
  44. 为了得到最优的运行速度,并发线程数等于处理器内核数。在这种情况下,就应当使用固定线程池,即并发线程总数有一个上限。
  45. 单线程执行器则对与性能分析很有帮助。如果临时用一个单线程执行器替换缓存或固定线程池,就能测量不使用并发的情况下应用的运行速度会慢多少。
  46. 可用下面的方法之一将Runnable或Callable对象提交给ExecutorService:
    Futuresubmit(Callable<T task)
    Future<?> submit(Runnable task)
    Futuresubmit(Runnable task, T result)
    线程池会在方便的时候尽早执行提交的任务。调用submit时,会得到一个 Future对象,可用来得到结果或者取消任务。
    第二个submit方法返回一个看起来有些奇怪的Future<?>。可以使用这样一个对象来调用isDone、cancel或isCancelled。但是,get方法在完成的时候只是简单地返回null。
    第三个版本的Submit也生成一个Future,它的get方法在完成的时候返回指定的result对象。
  47. 使用完一个线程池时,调用shutdown。这个方法启动线程池的关闭序列。被关闭的执行器不再接受新的任务。当所有任务都完成时,线程池中的线程死亡。另一种方法是调用shutdownNow。线程池会取消所有尚未开始的任务。
  48. 下面总结了在使用连接池时所做的工作:
    1.调用Executors类的静态方法newCachedThreadPool或newFixedThreadPool.
    2.调用 submit提交Runnable或 Callable对象。
    3.保存好返回的Future对象,以便得到结果或者取消任务。
    4.当不想再提交任何任务时,调用shutdown。
  49. fork-join框架:将一个任务分解成子任务,最后合并子任务的结果。
  50. 在后台,fork-join框架使用了一种有效的智能方法来平衡可用线程的工作负载,这种方法称为工作密取(work stealing)。每个工作线程都有一个双端队列(deque)来完成任务。每个工作线程将子任务压入其双端队列的队头。(只有一个线程可以访问队头,所以不需要加锁。)一个工作线程空闲时,它会从另一个双端队列的队尾“密取”一个任务。由于大的子任务都在队尾,这种密取很少出现。
    警告:fork-join池是针对非阻塞工作负载优化的。如果向一个 fork-join池增加很多阻塞任务,会让它无法有效工作。

猜你喜欢

转载自blog.csdn.net/qq_45254908/article/details/107206583