Java Core(第 11 版)笔记

发布时间 2023-11-07 21:09:59作者: MeYokYang

Java Core

第1章 Java程序设计概述

第2章 Java程序设计环境

第3章 Java的基本程序设计结构

3.1.一个简单的Java应用程序

  • 在Java1.4及以后版本,main方法必须为public。
  • 如果希望在终止程序时返回其它的退出码,使用System.exit

3.2.注释

3.3.数据类型

  • Java的8种基本类型:int、short、long、byte、float、double、char、boolean。无任何unsigned形式,要使用unsigned byte可使用Byte.toUnsingedInt(b),得到0-255的int值,处理完后在转换为byte。
  • 在十六进制中,使用p来表示指数。
  • Double.POSITIVE_INFINITYDouble.NEGATIVE_INFINITYDouble.NaN分别表示正无穷大、负无穷大、NaN。正数/0为Double.POSITIVE_INFINITY,0/0或者负数平方根为NaN。非数值的值都认为是不相同的,因此不要使用x == Double.NaN而使用Double.isNaN(x)
  • 在Java中,char类型描述了UTF-16编码中的一个代码单元。

3.4.变量与常量

  • 声明一个变量后,必须使用赋值语句对其显示初始化。使用未初始化的变量的值被Java编译器认为是错误的。

  • 在 Java 中不区分变量的定义与声明。

  • const是Java的保留字,但目前并没有使用。在Java中,必须使用final定义常量。

3.5.运算符

  • 整数被0除将会得到一个异常,而浮点数被0除将会得到无穷大或NaN的结果。

  • 使用strictfp关键字标记的方法必须使用严格的浮点运算来生成可再生的结果。

  • n % 2中,n(int类型)为负,这个表达式则为-1。

  • 如果一个计算溢出,数学运算符只是悄悄地返回错误结果而不做任何提醒。1_000_000_000 * 3返回-1294867296。使用Math.multiplyExact(1_000_000_000, 3)可抛出异常。

  • 数值之间的合法转换:

    虚线箭头表示可能有精度损失。

  • 对浮点数舍入运算使用Math.round(x)方法:int nx = (int) Math.round(9.997)。round方法返回long类型。

  • >>>运算符使用0填充高位,>>使用符号位填充高位,不存在<<<运算符。移位运算符的右操作符要完成模32(long为64)的运算。如1 << 35等同于1 << 3

3.6.字符串

  • 如果需要把多个字符串放在一起,用一个界定符分隔,使用join方法。如String all = String.join(" / ", "S", "M", "L", "XL");

  • Java11提供repeat方法:String repeated = "Java".repeat(3);

  • 一定不要使用==检测两个字符串是否相等。只有字符串字面量是共享的,而+或substring等操作得到的字符串不共享。

  • Java字符串由char值序列组成。

    虚拟机不一定把字符串实现为代码单元序列。在Java9中,只包含单字节代码单元的字符串使用byte数组实现,其它字符串使用char数组。

  • StringBuffer运行多线程的方式添加或删除字符。如果所有字符串编辑操作都在单个线程中进行,使用StringBuilder。

3.7.输入与输出

  • 当使用类不是定义在基本java.lang包中时,一定要使用import指令导入相应的包。
  • 将对象转换为字符串,对于实现了Formattable接口的任意对象,将调用这个对象的formatTo方法,否则调用toString方法。
  • 读取文本文件时,省略字符编码会使用运行这个Java程序的机器的默认编码。当指定一个相对文件名时,文件位于相对于Java虚拟机启动目录的位置。

3.8.控制流程

  • 编译代码时,添加-Xlint:fallthrough可对于switch分支缺少break语句时编译器会发出警告信息,使用@SuppressWarning("fallthrough")的除外。
  • 当在switch语句中使用枚举常量时,不必再每个标签中指明枚举名。

3.9.大数

  • BigInteger和BigDecimal实现任意精度的整数运算和浮点数运算。但不能使用算术运算符处理大数,而使用大数类中的方法。
  • Java并没有提供运算符重载功能,但他重载了字符串的+运算符。

3.10.数组

  • 长度为0的数组与null并不相同。

  • 创建一个数组时,所有元素都被初始化为0/false/null。获得数组长度可使用array.length

  • for each循环可适用于一个数组或是实现了Iterable接口的类对象。

  • 数组拷贝可使用Arrays类的copyof方法。

  • 在Java应用程序的main方法中,程序名并未存储在args数组中。

  • Arrays的sort方法使用了优化的快速排序算法。

  • 想要快速打印一个二维数组的数据元素列表,可使用System.out.println(Arrays.deepToString(a));

  • Java中,double [][] balances = new double[10][6];相当于C++的:

    double** balances = new double*[10];
    for (i = 0; i < 10; i++)
    {
        balances[i] = new double[6];
    }
    

    对于不规则数组,只能单独地分配行数组。

第4章 对象与类

4.1.面向对象程序设计概述

4.2.使用预定义类

  • 可以把Java中的对象变量看作类似于C++的对象指针。
  • 在Java中,必须使用clone方法获得对象的完整副本。
  • 标准Java类库分别包含了几个时间相关类:表示时间点的Date类、日历表示法表示日期的LocalDate类和GregorianCalendar类。不要使用构造器来构造LocalDate类对象,应当使用静态工厂方法,他会代表你使用构造器。LocalDate的plusDays方法返回新类,GregorianCalendar的add是一个更改器方法。

4.3.用户自定义类

  • 源文件名必须与public类的名字相匹配,一个源文件只能有一个公共类,但可以有任意数目的非公共类。

  • 不要在构造器中定义与实列字段同名的局部变量。

  • 不要对数值类型使用var。var关键字只能用于方法中的局部变量,参数和字段的类型必须声明。

  • if (n == null) name = "unknown"; else name = n;可使用name = Objects.requireNonNullElse(n, "unknown");代替。而requireNonNull方法则会直接拒绝null参数。

  • Java中的所有方法必须在类中定义,是否为内联方法是Java虚拟机的任务。

  • 不要编写返回可变对象引用的访问器方法。如果需要返回一个可变对象的引用,首先应该对它进行clone。

    class Employee {
        ...
        public Date getHireDay() {
            return (Date) hireDay.clone();
        }
        ...
    }
    
  • 对于以下方法是可行的:

    class Employee {
        ...
        public boolean equals(Employee other) {
            return name.equals(other.name);
        }
        ...
    }
    

    对于harry.equals(boss)中,方法访问harry的私有字段肯定是可以的,而对于boss的私有字段,由于boss是Employee类型的对象,equals是Employee的方法,所以访问boss的私有字段也是可以的。

  • final关键字只是表示在变量中的对象引用不会再指示另一个不同的对象,但该对象仍然可以修改。

4.4.静态字段与静态方法

  • System的setOut是一个原生方法(native),原生方法可以绕过Java语言的访问控制机制,所以它可修改被初始化为null的out(public static final PrintStream out = null;)。

4.5.方法参数

  • Java总是采用按值调用。对象引用是按值传递的。

4.6.对象构造

  • 如果在构造器中没有显示地为字段设置初值,那么就会被自动地赋为默认值0/false/null。

C++中,一个构造器不能调用另一个构造器。

4.7.包

  • Java中的package和import语句类似于C++中的namespace和using指令。
  • 从1.2版开始,JDK的实现者修改了类加载器,明确地禁止加载包名以“java.”开头地用户自定义地类。

4.8.JAR文件

4.9.文档注释

4.10.类设计技巧

第5章 继承

5.1.类、超类和子类

  • Java中所有继承都是公共继承。

  • super不是一个对象的引用,例如不能将其赋给另一个变量。它只是一个指示编译器调用超类方法的特殊关键字。

  • 如果子类构造器没有显式调用超类构造器,将自动地调用超类地无参构造器。

  • Java中动态绑定是默认行为,如果不希望让某一个方法是virtual,可以将其标记为final。

  • 允许子类将覆盖方法地返回类型改为原返回类型地子类型,被称为有可协变的返回类型。

  • 如果方法是private、static、final或者构造器,那么编译器将可以准确地知道应该调用那个方法,这称为静态绑定。

  • 在覆盖一个方法时,子类方法不能低于超类方法地可见性。

  • final类中的所有方法自动称为final方法,不包括字段。

  • Java中的Manager boss = (Manager) staff[1];类似C++的Manager* boss = dynamic<Manager*>(staff[1]);,前者强制类型转换失败时会抛出异常,后者则是返回null,可以用以下方式来完成类型测试与类型转换:

    if (staff[1] instanceof Manager) {
        Manager boss = (Manager) staff[1];
        ...
    }
    
    Manager* boss = dynamic<Manager*>(staff[1]);
    if (boss != nullptr) {
        ...
    }
    
  • 包含抽象方法的类本身必须被声明为抽象的。即是不含抽象方法,也可将类声明为抽象类。抽象类不能实例化。

5.2.Object:所有类的超类

  • 在Java中,只有基本类型不是对象。所有数组类型都扩展了Object类。

  • 比较两个对象时,防止出现null,可使用Objects.equals方法。

  • Java语言规范要求equals方法具有下面的特征:

    1. 自反性:对于任何非空引用x,x.equals(x)应该返回true。
    2. 对称性:对于任何引用x和y,当且仅当y.equals(x)返回true时,x.equals(y)返回true。
    3. 传递性:对于任何引用x、y和z,如果x.equals(y)返回true、y.equals(z)返回true,那么x.equals(z)也应该返回true。
    4. 一致性:如果x和y引用对象没有发生变化,反复调用x.equals(y)应该返回相同的结果。
    5. 对于任何非空引用x,x.equals(null)应该返回false。

    如果子类可以有自己相等性的概念,那么对称性需求将强制使用getClass检测。如果超类决定相等性概念,那么就可以使用instanceof检测,这样可以在不同子类的对象之间进行比较。

    以下给出编写一个完美equals方法的建议:

    1. 显式参数命名为otherObject(Object类),稍后需要将其强制转换为另一个名为other的变量。

    2. if (this == otherObject) return false;
      
    3. if (otherObject == null) return false;
      
    4. 如果equals语义可以在子类中改变,使用if (getClass() != otherObject.getClass()) return false;,如果所有子类都具有相同的相等性语言,使用if (!(otherObject instanceof ClassName)) return false;

    5. ClassName other = (ClassName) otherObject;

    6. 根据相等性概念的要求来比较字段,使用==比较基本类型字段,使用Objects.equals比较对象字段。

      return field1 == other.field1
          && Objects.equals(field2, other.field2)
          && ...;
      
  • 字符串的散列码是由内容导出的,对象是由对象的存储地址导出的。

  • 计算hash时,最好使用null安全的Objects.hashCode(x)而不是x.hash()

  • equals与hashCode的定义必须相容。

  • Arrays.hashCode方法计算数组的散列码是用数组元素。

5.3.泛型数组列表

  • 使用ArrayList类,在填充数组前可使用ensureCapacity方法来确保数组长度,以免填充数组时带来的数组copy开销。一旦确认数组列表大小将保持恒定,不在发生变化,就可调用trimToSize方法。
  • **将原始ArrayList赋给类型化ArrayList会得到警告,即使使用强制类型转化。出于兼容性考虑,编译器检查到没有发现违反规则的现象后,所有类型化数组列表转换成原始ArrayList对象,程序运行时,虚拟机中没有类型参数,所以ArrayList与ArrayList<...>执行相同的运行时检查。 **

5.4.对象包装器与自动装箱

  • 自动装箱规范要求boolean、byte、小于127的char、-128到127之间的short和int被包装到固定的对象中。所以使用==不一定相等,使用equals更好。

  • 装箱、拆箱是编译器的工作而不是虚拟机。

  • Integer对象是不可变的。要想修改数值可使用持有者类型,如IntHolder、BooleanHoulder等(位于org.omg.CORBA包,在JDK11已被移除),如:

    public static void triple(IntHolder x) {
        x.value = 3 * x.value;
    }
    

5.5.参数数量可变的方法

5.6.枚举类

  • 枚举类的构造器总是私有的,可以省略private修饰符。所有的枚举类型都是Enum的子类。

5.7.反射

  • T.class中T可以是void关键字。

  • Class对象实际上表示的一个类型,可能是类也可能不是,如int不是类,但int.class是一个Class对象。

  • Class其实是泛型类。

  • 鉴于历史原因,Class的getName方法作用数组会返回奇怪的名字,如[Ljava.lang.Double;

  • 虚拟机为每一个类型管理唯一的一个Class对象,因此可以使用==比较。

  • Class类似于C++的type_info类,但功能更全面,getClass方法等价于C++的typeid运算符。

  • Class类中的getFields、getMethods、getConstructors方法将分别返回这个类支持的公共字段、方法和构造器数组,包括超类的。Class类中的getDeclareFields、getDeclareMethods、getDeclareConstructors方法返回类中的所有字段、方法和构造器数组,不包括超类的

  • 反射机制的默认行为受限于Java的访问控制,不过可使用setAccessible方法覆盖Java的访问控制。如果不允许访问(访问可以被模块系统或安全管理器拒绝),setAccessible调用会抛出异常。

  • 一个copy数组的函数:

    public static Object goodCopyOf(Object a, int newLength) {
        Class cl = a.getClass();
        if (!cl.isArray()) return null;
        Class componentType = cl.getComponentType();
        int length = Array.getLength(a);
        Object newArray = Array.newInstance(componentType, newLength);
        System.arraycopy(a, 0, newArray, 0, Math.min(length, newLength));
        return newArray;
    }
    

    首先这里数组参数为Object,而不是Object[],因为基本数据类型数组可以转换为Object而不能转换为Object[]。其次,新数组中存储类型为原本数组中存储的类型而不是Object,为了返回数组后能够成功强制类型转换。

  • invoke方法:Object invoke(Object obj, Object... args),第一个参数是隐式参数,其余的对象提供了显式参数,对于静态方法第一个参数可设为null。

  • 反射程序示例:

    package reflection;
    
    import java.util.*;
    import java.lang.reflect.*;
    
    public class ReflectionTest {
    
        public static void main(String[] args) throws ClassNotFoundException {
            String name;
            if (args.length > 0) {
                name = args[0];
            } else {
                Scanner in = new Scanner(System.in);
                System.out.println("Enter class name (e.g. java.util.Date): ");
                name = in.next();
            }
            Class cl = Class.forName(name);
            Class supercl = cl.getSuperclass();
            String modifiers = Modifier.toString(cl.getModifiers());
            if (modifiers.length() > 0) { System.out.print(modifiers + " "); }
            System.out.printf("class " + name);
            if (supercl != null && supercl != Object.class) {
                System.out.printf(" extends " + supercl.getName());
            }
    
            System.out.printf("\n{\n");
            printConstructors(cl);
            System.out.println();
            printMethods(cl);
            System.out.println();
            printFields(cl);
            System.out.println("}");
        }
    
        public static void printConstructors(Class cl) {
            Constructor[] constructors = cl.getConstructors();
            for (Constructor c : constructors) {
                String name = c.getName();
                System.out.printf("    ");
                String modifiers = Modifier.toString(c.getModifiers());
                if (modifiers.length() > 0) { System.out.print(modifiers + " "); }
                System.out.print(name + "(");
    
                Class[] paramTypes = c.getParameterTypes();
                for (int j = 0; j < paramTypes.length; j++) {
                    if (j > 0) { System.out.print(", "); }
                    System.out.print(paramTypes[j].getName());
                }
                System.out.println(");");
            }
        }
    
        public static void printMethods(Class cl) {
            Method[] methods = cl.getDeclaredMethods();
            for (Method m : methods) {
                Class retType = m.getReturnType();
                String name = m.getName();
    
                System.out.print("    ");
                String modifiers = Modifier.toString(m.getModifiers());
                if (modifiers.length() > 0) {
                    System.out.print(modifiers + " ");
                }
                System.out.print(retType.getName() + " " + name + "(");
                Class[] paramTypes = m.getParameterTypes();
                for (int j = 0; j < paramTypes.length; j++) {
                    if (j > 0) { System.out.print(", "); }
                    System.out.print(paramTypes[j].getName());
                }
                System.out.println(");");
            }
        }
    
        public static void printFields(Class cl) {
            Field[] fields = cl.getDeclaredFields();
            
            for (Field f : fields) {
                Class type = f.getType();
                String name = f.getName();
                System.out.print("    ");
                String modifiers = Modifier.toString(f.getModifiers());
                if (modifiers.length() > 0) { System.out.print(modifiers + " "); }
                System.out.println(type.getName() + " " + name + ";");
            }
        }
    }
    
    

5.8.继承的设计技巧

第6章 接口、lambda表达式和内部类

6.1.接口

  • Java5的Comparable接口已经提升为一个泛型类型,仍然可以使用不带类型参数的Comparable接口,这样的话类型参数为Object。

  • 接口中所有的方法都自动是public的。在实现接口时,需要把方法声明为public。接口可以定义字段,且自动为public static final。没必要在接口中添加这些自动的关键字。

  • Comparable接口的文档建议compareTo方法应当与equals方法兼容,Java API中大多遵循了这个建议。有一个重要例外为BigDecimal,new BigDecimal("1.0")new BigDecimal("1.00")由于精度不同,equals为false,但compareTo为0。

  • Arrays的sort方法除基本类型数组外被定义为接受Object[]数组,对于该数组会被强制类型转换为Comparable,并调用compareTo方法。所以没有实现Comparable接口会在虚拟机时抛出异常而不是编译器。

  • Java8中,允许在接口中添加静态方法。Java9中,接口的方法可以是private。

  • 对于默认方法的冲突,遵循超类优先,即超类有该方法则接口的默认方法被忽略。如果多个接口提供了相同方法,且至少有一个提供了默认方法实现,那么实现类必须覆盖这个方法来解决冲突。覆盖时可以选择冲突方法之一,如:

    interface Person {
        default String getName() { return ""; }
    }
    
    interface Named {
        default String getName() { return getClass().getName() + "_" + hashCode(); }
    }
    
    class Student implements Person, Named {
    
        @Override
        public String getName() {
            return Person.super.getName();
        }
    }
    
  • Object的clone方法是一个protected native的方法,是浅拷贝(因为是原生实现,不要纠结父类是怎么知道子类有哪些字段并拷贝的,其实我也不知道是怎么实现的,JDK11闭源)。之所以是protected,目的是拒绝直接调用Object的clone方法(除子类重写clone方法或子类其它方法要使用父类clone),而让子类自己重写为public方法并实现自己的定义(可将其实现为深拷贝,可指定哪些引用类型深拷贝,Object是不知道子类有哪些引用类型所以没法指定哪些字段深拷贝)。在含调用Object的clone方法的类中,必须实现Cloneable标记接口,否则会生成CloneNotSupportedException异常。

  • 重写Object的clone的子类,除非是final,否则对于CloneNotSupportedException异常最好是throws而不是try-catch。

  • Java1.4之前,clone方法返回类型为Object,之后可以为覆盖的clone方法指定正确的返回类型(协变)。

  • 所有数组都有一个public的clone方法。

6.2.lambda表达式

  • 在Java中,对lambda表达式所能做的也只是转换为函数式接口。

  • java.util.function包中BiFunction<T, U, R>接口描述了参数类型为T、U而且返回R类型的函数。

  • ArrayList的removeIf方法,参数为Predicate函数式接口。

  • java.util.function包中Supplier<T>接口用于懒计算。如以下示例,预计day很少为空,requireNonNullElseGet只在需要值时才调用Supplier,也就是创建LocalDate对象。

    LocalDate hireDay1 = Objects.requireNonNullElse(day, LocalDate.of(1920, 1, 1));
    LocalDate hireDay2 = Objects.requireNonNullElseGet(day, () -> LocalDate.of(1920, 1, 1));
    
  • 方法引用的三种形式:

    1. object::instanceMethod:等价于向方法传递参数的lambda表达式。如Syatem.out::println等价于x -> System.out.println(x)
    2. Class::instanceMethod:第一个参数会成为方法的隐式参数。如String::compareToIgnoreCase等价于(x, y) -> x.compareToIgnoreCase(y)
    3. Class::staticMethod:所有参数都传递到静态方法。如Math::pow等价于(x, y) -> Math.pow(x, y)

    示例及说明:

  • 只有当lambda表达式的体只调用一个方法而不做其它操作时,才能把lambda表达式重写为方法引用。如s -> s.length() == 0就不能重写为方法引用。

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

  • 包含对象的方法引用与等价的lambda表达式有一个细微区别,如当separator对象为null时,separator::equals在构造时就会抛出NullPointerException异常,而x -> separator.equals(x)只在调用时抛出NullPointerException异常。

  • 可以在方法引用中使用this、super参数,如super::great

  • 在lambda表达式中,只能引用值不会改变的变量。lambda捕获的变量必须实际上是事实最终变量,即这个变量初始化后就不会再为它赋新值。对于该自由变量,lambda转换为对象时,会将其复制到这个对象的实例变量中。如以下示例,调用repeatMessage("Hello,", 1000),lambda可能在调用返回很久后才执行,对于text参数已不存在,而lambda转换为对象会把该变量保存在对象中:

    public static void repeatMessage(String text, int delay) {
        ActionListener listener = event -> {
            System.out.printf(text);
            Toolkit.getDefaultToolkit().beep();
        };
        new Timer(delay, listener).start();
    }
    
  • 在lambda表达式中使用this关键字,指的是创建这个lambda表达式的方法的this参数。但在lambda表达式中,this的使用并没有任何特殊之处。

  • 使用lambda表达式的重点是延迟执行。

  • 常用函数式接口:

  • 基本类型int、long、double的34个可用的特殊化接口:

  • 大多标准函数式接口都提供了非抽象方法生成或合并函数。如Predicate.isEqual(a)等同于a::equals不过a为null也能工作。已经提供了默认方法and、or和negate来合并谓词,如Predicate.isEqual(a).or(Predicate.isEqual(b))等同于x -> a.equals(x) || b.equals(x)

  • 如果设计自己的函数时接口,可添加@FunctionalInterface注解。

  • Comparator接口包含很多方便的静态方法来创建比较器,这些方法可用于lambda表达式或方法引用。

    // TODO 2023/11/6 meyok: 254页
    Arrays.sort(people, Comparator.comparing(Person::getName));
    Arrays.sort(people, Comparator.comparing(Person::getLastName).thenComparing(Person::getFirstName));
    Arrays.sort(people, Comparator.comparing(Person::getName, (s, t) -> Integer.compare(s.length(), t.length())));
    Arrays.sort(people, Comparator.comparingInt(p -> p.getName().length()));
    ...
    

6.3.内部类

  • 内部类的实现主要有两个原因:

    1. 内部类可以对同一个包中的其他类隐藏。
    2. 内部类方法可以访问定义这个类的作用域中的数据,包括原本私有的数据。
  • 考虑到以下类:

    package innerClass;
    
    import javax.swing.*;
    import java.awt.*;
    import java.awt.event.ActionEvent;
    import java.awt.event.ActionListener;
    import java.time.Instant;
    
    public class InnerClassTest {
        public static void main(String[] args) {
            TalkingClock clock = new TalkingClock(1000, true);
            clock.start();
            
            JOptionPane.showMessageDialog(null, "Quit program?");
            System.exit(0);
        }
    }
    
    class TalkingClock {
        private int interval;
        private boolean beep;
    
        public TalkingClock(int interval, boolean beep) {
            this.interval = interval;
            this.beep = beep;
        }
    
        public void start() {
            TimePrinter listener = new TimePrinter();
            Timer timer = new Timer(interval, listener);
            timer.start();
        }
        
        public class TimePrinter implements ActionListener {
    
            @Override
            public void actionPerformed(ActionEvent e) {
                System.out.println("At the tone, the time is " + Instant.ofEpochSecond(e.getWhen()));
                if (beep) { Toolkit.getDefaultToolkit().beep(); }
            }
        }
    }
    
    
    • 创建外部类并不意味着有内部类实例字段。上述示例中内部类是外部类的start方法创建的。

    • Java内部类的对象会有一个隐式引用,指向实例化这个对象的外部类对象,通过该指针可访问外部对象的全部状态。但Java静态内部类没有这个附加的指针,所以Java静态内部类相当于C++嵌套类。上述示例中,其实编译器会修改所有内部类的构造器,添加一个对应外围类的引用参数:、

      public TimePrinter(TalkingClock clock) {
          ...
      }
      

      上述外围类start方法中new TimePrinter()时会将外围类对象引用this传递给修改后的内部类构造器

      TimePrinter listener = new TimePrinter(this);
      

      也可使用指定的外围类对象创建内部类,使得内部类有引用指定外围类对象的引用而不是执行该动作的对象:

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

      由于内部类引用有外部类对象的引用,可以访问外围类对象的字段。如上述内部类方法actionPerformed访问了外围类对象的beep字段,实际上更正规引用(编译器编译后的写法也是这个)为TalingClock.this.beep

    • 内部类是一个编译器现象,与虚拟机无关。编译器会把内部类转换为常规的类文件,用$分隔外部类名与内部类名,而虚拟机对此一无所知。使用javap解析内部类的作用,可看到编译器实际生成了this$0引用外部类:

      javap -private innerClass.TalkingClock\$TimePrinter
      
      public class innerClass.TalkingClock$TimePrinter implements java.awt.event.ActionListener {
        final innerClass.TalkingClock this$0;
        public innerClass.TalkingClock$TimePrinter(innerClass.TalkingClock);
        public void actionPerformed(java.awt.event.ActionEvent);
      }
      

      而该内部类之所以可通过this$0引用访问该对象的私有字段,实际上是因为外部类生成了一个根据引用访问字段的方法,编译器实际是调用该方法。如内部类访问beep,编译器实际添加了一个方法access$0

      class innerClass.TalkingClock {
        private int interval;
        private boolean beep;
        
        static boolean access$0(TalkingClock);
        public void start();
        
        public innerClass.TalkingClock(int, boolean);
      }
      

      而内部类使用beep实际是TalingClock.access$0(this$0)。生成的access$0方法可能被黑客用来攻击。

  • 可以把内部类声明为私有,这样只有外围类可构造内部类对象,但只有内部类可以是私有的如果将上述示例中内部类声明为private,编译器将内部类转换为基本类,但虚拟机中不存在私有类,所以编译器将私有内部类转换为具有包可见性的基本类,并具有以下构造器

    private TalkingClock$TimePrinter(TalkingClock);
    TalkingClock$TimePrinter(TalkingClock, TalkingClock$1);
    

    第一个构造器外部无法调用。第二个构造器将调用第一个构造器,还有一个合成的TalkingClock$1类型作为参数以区分这两个构造器。上述外部类start方法实际上是调用:

    new TalkingClock$TimePrinter(this, null);
    
  • 内部类声明的所有静态字段都必须是final,并初始化为一个编译时常量。

  • 内部类不能有static方法,Java语言规范没有对该限制做任何解释。也允许有静态方法(static内部类中),但只能访问外围类的静态字段和方法。

  • 可将内部类声明在方法中,被称为局部内部类。如:

    public void start(int interval, boolean beep) {
        
        class TimePrinter implements ActionListener {
            @Override
            public void actionPerformed(ActionEvent e) {
                System.out.println("At the tone, the time is " + Instant.ofEpochSecond(e.getWhen()));
                if (beep) {
                    Toolkit.getDefaultToolkit().beep();
                }
            }
        }
    
        TimePrinter listener = new TimePrinter();
        Timer timer = new Timer(interval, listener);
        timer.start();
    }
    

    局部内部类不能有访问说明符,其作用域被限定在声明这个局部类的块中。

    局部内部类不仅能访问外部类的字段,也可以访问局部变量,不过该局部变量必须是事实最终变量。实际上编译器会在生成一个用于保存该变量的字段,创建局部内部类时,会将该变量传递给构造器(构造器也做了相应修改),并存储在该字段中。如上述示例中beep被修改为start的参数而不是外部类的字段,局部类可访问该事实最终变量,该变量会被赋值一份保存在val$beep中:

    class TalkingClock$TimePrinter {
      final TalkingClock this$0;
      final boolean val$beep;
      public TalkingClock$TimePrinter(TalkingClock, boolean);
      public void actionPerformed(java.awt.event.ActionEvent);
    }
    
    
  • 创建一个类的对象,甚至可以不为该类指定名字,这样一个类被称为匿名内部类。如:

    var listener = new ActionListener() {
        @Override
        public void actionPerformed(ActionEvent e) {
            System.out.println("At the tone, the time is " + Instant.ofEpochSecond(e.getWhen()));
            if (beep) {
                Toolkit.getDefaultToolkit().beep();
            }
        }
    };
    

    实际上是创建了一个新的内部类,该类实现了对应接口(也可以是拓展了某个类,成为该类的子类),并使用该新的内部类创建了对象。

    由于该新的内部类匿名,所以该内部类不存在构造器。实际上,构造参数被传递给超类构造器,若为接口就不能有构造参数。

    匿名内部类可提供一个对象初始化块,如:

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

    对匿名子类重写equals方法,如果使用if (getClass() != other.getClass()) { return false; }可能会失败。

    静态方法使用getClass()不奏效(因为实际调用this.getClass(),静态方法没有this),可使用new Object() {}.getClass().getEnclosingClass()

  • 将内部类声明为static为静态内部类(又被称为嵌套类)。静态内部类可以有静态字段和静态方法。

  • 接口中声明的内部类自动是staticpublic

6.4.服务加载器

6.5.代理

  • 只有在编译时期无法确定需要实现哪个接口时才有必要使用代理。

  • 构造一个具体的类可使用newInstance方法或使用反射找出构造器,但不能实例化接口,需要在运行的程序中给定一个新类。利用代理可以在运行时创建实现了一组给定接口的新类

  • 代理类包含以下方法:

    1. 指定接口所需要的全部方法。
    2. Object类中的全部方法,例如toStringequals等。

    不过不能再运行时为这些方法定义新代码。

  • 创建代理对象需要一个类加载器、一个Class对象数组(元素为需要实现的各个接口)、一个调用处理器。

    调用处理器是实现了InvocationHandler接口的类的对象,该接口只包含一个invoke方法:

    public interface InvocationHandler {
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable;
    }
    

    调用代理对象的方法时,调用处理器的invoke方法会被调用,向其传递Method对象和原调用的参数。

  • 代理类是再程序运行时动态创建的,一旦被创建,它与虚拟机中的其他类没有任何区别。

  • 所有代理类都扩展Proxy类。一个代理类只有一个实例字段——调用处理器,他在Proxy超类中定义。完成代理对象任务所需要的任何额外数据都必须存储再调用处理器中。

  • 没有定义代理类的名字,Oracle虚拟机中的Proxy类将生成一个以字符串$Proxy开头的类名。

  • 对于一个特定的类加载器和预设的一组接口来说,只能有一个代理类。

  • 代理类总是public final 。如果代理类实现的所有接口都是public,这个代理类就不属于任何特定的包;否则,所有非公共接口必须属于同一个包,代理类也就属于这个包。

  • 使用Proxy.getProxyClass(null, interfaces)获取对应代理类的Class。

  • 使用ProxyisProxyClass方法检测一个特定的Class对象是否表示一个代理类。

  • 代理类示例:

    package proxy;
    
    import java.lang.reflect.InvocationHandler;
    import java.lang.reflect.Method;
    import java.lang.reflect.Proxy;
    import java.util.Arrays;
    import java.util.Random;
    
    public class ProxyTest {
        public static void main(String[] args) {
            Object[] elements = new Object[1000];
    
            for (int i = 0; i < elements.length; i++) {
                Integer value = i + 1;
                TraceHandler handler = new TraceHandler(value);
                Object proxy = Proxy.newProxyInstance(
                        ClassLoader.getSystemClassLoader(),
                        new Class[] {Comparable.class},
                        handler
                );
                elements[i] = proxy;
            }
    
            Integer key = new Random().nextInt(elements.length) + 1;
            int result = Arrays.binarySearch(elements, key);
            if (result > 0) { System.out.println(elements[result]); }
        }
    }
    
    class TraceHandler implements InvocationHandler {
        
        private Object target;
    
        public TraceHandler(Object target) {
            this.target = target;
        }
    
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            
            System.out.print(target);
            System.out.print("." + method.getName() + "(");
            if (args != null) {
                for (int i = 0; i < args.length; i++) {
                    System.out.println(args[i]);
                    if (i < args.length - 1) { System.out.println(", "); }
                }
            }
            System.out.println(")");
            
            return method.invoke(target, args);
        }
    }
    

    Integer类实际实现了Comparable,但运行时所有的泛型被取消,会用对应原始Comparable类的类对象构造代理。

    尽管toString不属于Comparable,但仍然被代理。

第7章 异常、断言和日志

7.1.处理错误