Java核心技术1-继承、接口

读《Java核心技术1》记录知识点

继承

类、超类和子类

  1. 利用继承,人们可以基于已存在的类构造一个新类。继承已存在的类就是复用(继承)这些类的方法和域。在此基础上,还可以添加一些新的方法和域, 以满足新的需求。这是 Java 程序设计中的一项核心技术。

  2. 反射是指在程序运行期间发现更多的类及其属性的能力。

  3. 可以使用特定的关键字 super调用父类的方法。

  4. 可以通过 super 实现对超类构造器的调用。使用super 调用构造器的语句必须是子类构造器的第一条语句。

  5. 如果子类的构造器没有显式地调用超类的构造器, 则将自动地调用超类默认(没有参数 )的构造器。 如果超类没有不带参数的构造器, 并且在子类的构造器中又没有显式地调用超类的其他构造器,则 Java 编译器将报告错误。

  6. 一个对象变量(例如, 变量 e ) 可以指示多种实际类型的现象被称为多态( polymorphism)。
    在运行时能够自动地选择调用哪个方法的现象称为动态绑定(dynamic binding)。

  7. 在 Java 中, 不需要将方法声明为虚拟方法。动态绑定是默认的处理方式。如果不希望让一个方法具有虚拟特征, 可以将它标记为 final

  8. 对于 final 域来说,构造对象之后就不允许改变它们的值了。 不过, 如果将一个类声明为 final, 只有其中的方法自动地成为 final,而不包括域。

  9. 为了提高程序的清晰度, 包含一个或多个抽象方法的类本身必须被声明为抽象的。

  10. 抽象方法之外,抽象类还可以包含具体数据和具体方法。

  11. 类即使不含抽象方法,也可以将类声明为抽象类。
    抽象类不能被实例化。也就是说, 如果将一个类声明为 abstract, 就不能创建这个类的对象

  12. 归纳一下 Java 用于控制可见性的 4 个访问修饰符:
    1 ) 仅对本类可见 private。
    2 ) 对所有类可见 public。
    3 ) 对本包和所有子类可见 protected。
    4 ) 对本包可见—默认(很遗憾,) 不需要修饰符。

Object:所有类的超类

  1. Object 类是 Java 中所有类的始祖, 在 Java 中每个类都是由它扩展而来的。
  2. 在 Java 中, 只有基本类型 ( primitive types) 不是对象, 例如,数值、 字符和布尔类型的值都不是对象。
  3. Object 类中的 equals 方法用于检测一个对象是否等于另外一个对象。
  4. 散列码( hash code ) 是由对象导出的一个整型值。 散列码是没有规律的。如果 x 和 y 是两个不同的对象, x.hashCode( ) 与 y.hashCode( ) 基本上不会相同。
  5. 如果重新定义 equals方法, 就必须重新定义 hashCode 方法, 以便用户可以将对象插入到散列表中。

泛型数组列表(ArrayList)

  1. ArrayList在添加或删除元素时, 具有自动调节数组容量的功能,而不需要为此编写任何代码。

  2. 数组列表管理着对象引用的一个内部数组。最终, 数组的全部空间有可能被用尽。这就显现出数组列表的操作魅力: 如果调用 add 且内部数组已经满了,数组列表就将自动地创建一个更大的数组,并将所有的对象从较小的数组中拷贝到较大的数组中。

  3. 如果已经清楚或能够估计出数组可能存储的元素数量, 就可以在填充数组之前调用
    ensureCapacity方法:
    staff.ensureCapacity(lOO);
    这个方法调用将分配一个包含 100 个对象的内部数组。然后调用 100 次 add, 而不用重新分配空间。
    另外,还可以把初始容量传递给 ArrayList 构造器:
    ArrayList staff = new ArrayListo(100) ;

  4. 一旦能够确认数组列表的大小不再发生变化, 就可以调用 trimToSize 方法。这个方法将存储区域的大小调整为当前元素数量所需要的存储空间数目。垃圾回收器将回收多余的存储空间。
    一旦整理了数组列表的大小,添加新元素就需要花时间再次移动存储块,所以应该在确认不会添加任何元素时, 再调用 trimToSize。

  5. 数组列表自动扩展容量的便利增加了访问元素语法的复杂程度。 其原因是 ArrayList 类并不是 Java 程序设计语言的一部分;它只是一个由某些人编写且被放在标准库中的一个实用类。
    使用 get 和 set 方法实现访问或改变数组元素的操作,而不使用人们喜爱的 [ ] 语法格式。

  6. 使用 list.set(i,x) 方法,它只能替换数组中已经存在的元素内容。

  7. 没有泛型类时, 原始的 ArrayList 类提供的get方法别无选择只能返回Object, 因此,get方法的调用者必须对返回值进行类型转换:
    Employee e = (Eiployee) staff.get(i);

  8. 原始的 ArrayList 存在一定的危险性。它的 add 和 set 方法允许接受任意类型的对象。对于下面这个调用
    staff.set(i,”Harry Hacker”);
    编译不会给出任何警告, 只有在检索对象并试图对它进行类型转换时, 才会发现有问题。如果使用 ArrayList, 编译器就会检测到这个错误。

  9. 对数组实施插人和删除元素的操作其效率比较低。对于小型数组来说, 这一点不必担心。但如果数组存储的元素数比较多, 又经常需要在中间位置插入、删除元素, 就应该考虑使用链表了。

  10. 类型化与原始数组列表的兼容性问题:一旦能确保不会造成严重的后果, 可以用@SuppressWamings(“unchecked”) 标注来标记这个变量能够接受类型转换, 如下所示:
    @SuppressWarnings (“unchecked”)

    ArrayList result =(ArrayList) employeeDB.find(query); // yields another warning

对象的包装器与自动装箱

  1. 对象包装器 :有时,需要将 int 这样的基本类型转换为对象。 所有的基本类型都有一个与之对应的类。例如,Integer 类对应基本类型 int。通常, 这些类称为包装器 ( wrapper ) 。

    这些对象包装器类拥有很明显的名字:Integer、Long、Float、Double、Short、Byte、Character 、 Void 和 Boolean (前6 个类派生于公共的超类 Number)。对象包装器类是不可变的,即一旦构造了包装器,就不
    允许更改包装在其中的值。同时, 对象包装器类还是 final , 因此不能定义它们的子类。

  2. 假设想定义一个整型数组列表。而尖括号中的类型参数不允许是基本类型,也就是说,不允许写成 ArrayList。这里就用到了 Integer 对象包装器类。 我们可以声明一个 Integer对象的数组列表。

  3. 如果在一个条件表达式中混合使用 Integer 和 Double 类型, Integer 值就会拆箱,提升为 double, 再装箱为 Double

参数数量可变的方法

  1. printf方法是这样定义的:
    public class PrintStream
    {
    public PrintStream printf(String fmt , Object… args) { return format(fmt, args); }
    }
    这里的省略号 . . . 是 Java 代码的一部分,它表明这个方法可以接收任意数量的对象(除 fmt参数之外)。

    实际上,printf 方法接收两个参数,一个是格式字符串,另一个是 Object [] 数组

枚举类

  1. 枚举类 :在比较两个枚举类型的值时, 永远不需要调用 equals, 而直接使用“ = =” 就可以了。

  2. 所有的枚举类型都是 Enum 类的子类。它们继承了这个类的许多方法。其中最有用的一个是 toString, 这个方法能够返回枚举常量名。 例如,Size.SMALL.toString( ) 将返回字符串“ SMALL”。

反射

  1. 能够分析类能力的程序称为反射( reflective )。
  2. 反射机制可以用来:
    • 在运行时分析类的能力。
    • 在运行时查看对象, 例如, 编写一个 toString 方法供所有类使用。
    • 实现通用的数组操作代码。
    • 利用 Method 对象, 这个对象很像C++中的函数指针。
  3. 在程序运行期间,Java 运行时系统始终为所有的对象维护一个被称为运行时的类型标识。这个信息跟踪着每个对象所属的类。 虚拟机利用运行时类型信息选择相应的方法执行。
  4. 可以通过专门的 Java 类访问这些信息。保存这些信息的类被称为 Class。
  5. Object 类中的 getClass( ) 方法将会返回一个 Class 类型的实例。
  6. 最常用的 Class 方法是 getName。 这个方法将返回类的名字。
  7. 还可以调用静态方法 forName 获得类名对应的 Class 对象。
  8. 虚拟机为每个类型管理一个 Class 对象。 因此, 可以利用= 运算符实现两个类对象比较的操作。 例如,
    if (e.getClass() == Employee.class)
    
  9. 还有一个很有用的方法 newlnstance( ), 可以用来动态地创建一个类的实例例如:
    e.getClass().newlnstance(); 
    
  10. 创建了一个与 e 具有相同类类型的实例。 newlnstance 方法调用默认的构造器 (没有参数的构造器)初始化新创建的对象。 如果这个类没有默认的构造器, 就会抛出一个异常 。将 forName 与 newlnstance 配合起来使用, 可以根据存储在字符串中的类名创建一个对象 。
  11. 如果没有提供处理器, 程序就会终止,并在控制台上打印出一条信息, 其中给出了异常的类型。可能在前面已经看到过一些异常报告, 例如, 偶然使用了 null 引用或者数组越界等。
  12. 异常有两种类型: 未检查异常和已检查异常。 对于已检查异常, 编译器将会检查是否提供了处理器。 然而, 有很多常见的异常, 例如, 访问 null 引用, 都属于未检查异常。 编译器不会査看是否为这些错误提供了处理器。毕竟,应该精心地编写代码来避免这些错误的发生, 而不要将精力花在编写异常处理器上。
  13. 如果类名不存在, 则将跳过 try 块中的剩余代码, 程序直接进人 catch 子句(这里,利用Throwable 类的 printStackTrace 方法打印出栈的轨迹。 Throwable 是 Exception 类的超类)。 如果 try 块中没有抛出任何异常, 那么会跳过 catch 子句的处理器代码。
  14. 对于已检查异常, 只需要提供一个异常处理器。 可以很容易地发现会抛出已检査异常的方法。如果调用了一个抛出已检查异常的方法, 而又没有提供处理器, 编译器就会给出错误报告。
  15. 反射机制最重要的内容—检查类的结构。

    在 java.lang.reflect 包中有三个类 Field、 Method 和 Constructor 分别用于描述类的域、 方法和构造器。 这三个类都有一个叫做 getName 的方法, 用来返回项目的名称。 Field类有一个 getType 方法, 用来返回描述域所属类型的 Class 对象。
    Method 和 Constructor 类有能够报告参数类型的方法, Method 类还有一个可以报告返回类型的方法。

    这 三个类还有一个叫做 getModifiers 的方法, 它将返回一个整型数值, 用不同的位开关描述 public 和 static 这样的修饰符使用状况。另外, 还可以利用 java.lang.refleCt 包中的 Modifier类的静态方法分析getModifiers 返回的整型数值。 例如, 可以使用 Modifier 类中的 isPublic、 isPrivate 或 isFinal判断方法或构造器是否是 public、 private 或 final。 我们需要做的全部工作就是调用 Modifier类的相应方法, 并对返回的整型数值进行分析, 另外,还可以利用 Modifier.toString方法将修饰符打印出来。

  16. Class类中的 getFields、 getMethods 和 getConstructors 方 法 将 分 别 返 回 类 提 供 的public 域、 方法和构造器数组, 其中包括超类的公有成员。Class 类的 getDeclareFields、getDeclareMethods 和 getDeclaredConstructors 方法将分别返回类中声明的全部域、 方法和构造器, 其中包括私有和受保护成员,但不包括超类的成员。

  17. 反射机制的默认行为受限于 Java 的访问控制。然而, 如果一个 Java 程序没有受到安全管理器的控制, 就可以覆盖访问控制。 为了达到这个目的, 需要调用 Field、 Method 或Constructor 对象的 setAccessible 方法。

  18. setAccessible 方法是 AccessibleObject 类中的一个方法, 它是 Field、 Method 和 Constructor类的公共超类。这个特性是为调试、 持久存储和相似机制提供的。

  19. get 方法还有一个需要解决的问题。name 域是一个 String, 因此把它作为 Object 返回没有什么问题。但是, 假定我们想要查看 salary 域。它属于 double 类型, 而 Java 中数值类型不是对象。要想解决这个问题, 可以使用 Field 类中的 getDouble方法,也可以调用 get方法, 此时, 反射机制将会自动地将这个域值打包到相应的对象包装器中, 这里将打包成Double。

  20. 调用任意方法:在 C 和 C++ 中, 可以从函数指针执行任意函数。从表面上看, Java 没有提供方法指针,即将一个方法的存储地址传给另外一个方法, 以便第二个方法能够随后调用它。事实上,Java 的设计者曾说过:方法指针是很危险的,并且常常会带来隐患。 他们认为 Java 提供的接口(interface)是一种更好的解决方案。然而,反射机制允许你调用任意方法。

  21. 在 Method 类中有一个 invoke 方法, 它允许调用包装在当前 Method 对象中的方法。

  22. 继承的设计技巧

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

接口

接口

  1. 在 Java 程序设计语言中, 接口不是类,而是对类的一组需求描述,这些类要遵从接口描述的统一格式进行定义。
  2. 接口中的所有方法自动地属于 public。 因此, 在接口中声明方法时, 不必提供关键字public。
  3. 提供实例域和方法实现的任务应该由实现接口的那个类来完成。因此, 可以将接口看成是没有实例域的抽象类 。
  4. 接口不是类,尤其不能使用 new 运算符实例化一个接口 。但却能声明接口的变量 。
  5. 接口变量必须弓I用实现了接口的类对象 。
  6. 同使用 instanceof检查一个对象是否属于某个特定类一样, 也可以使用instance 检查一个对象是否实现了某个特定的接口 。
  7. 与可以建立类的继承关系一样,接口也可以被扩展。这里允许存在多条从具有较高通用性的接口到较高专用性的接口的链。
  8. 在接口中不能包含实例域或静态方法,但却可以包含常量。
  9. 尽管每个类只能够拥有一个超类, 但却可以实现多个接口。这就为定义类的行为提供了极大的灵活性。
  10. 为什么已经有抽象类了,Java还要引入接口?因为每个类只能继承一个类 ,但可以实现多个接口。
  11. 可以为接口方法提供一个默认实现。 必须用 default 修饰符标记这样一个方法。
  12. 默认实现可用于接口升级,是一种接口演化。

    接口示例

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

    lambda表达式

  14. lambda 表达式是一个可传递的代码块, 可以在以后执行一次或多次。
  15. Java 中的一种 lambda 表达式形式:参数, 箭头(->) 以及一个表达式。如果代码要完成的计算无法放在一个表达式中,就可以像写方法一样,把这些代码放在 {}中,并包含显式的 return 语句。
  16. 即使lambda表达式没有参数,仍然要提供空括号,就像无参数方法一样。
  17. 如果可以推导出一个lambda表达式的参数类型,则可以忽略其类型。
  18. 如果方法只有一个参数,而且这个参数的类型可以推导得出,那么甚至还可以省略小括号。
  19. 无需指定lambda表达式的返回类型。

    内部类

  20. 内部类(inner class ) 是定义在另一个类中的类。为什么需要使用内部类呢? 其主要原因有以下三点:
    • 内部类方法可以访问该类定义所在的作用域中的数据, 包括私有的数据。
    • 内部类可以对同一个包中的其他类隐藏起来。
    • 当想要定义一个回调函数且不想编写大量代码时,使用匿名 (anonymous) 内部类比较便捷。
  21. 内部类既可以访问自身的数据域,也可以访问创建它的外围类对象的数据域。
  22. 外围类对象的引用称为 outer。
  23. 只有内部类可以是私有类,而常规类只可以具有包可见性,或公有可见性。
  24. 内部类中声明的所有静态域都必须是 final。
  25. 内部类不能有 static 方法。
  26. 局部类不能用 public 或 private 访问说明符进行声明。它的作用域被限定在声明这个局部类的块中。
  27. 与其他内部类相比较, 局部类还有一个优点。它们不仅能够访问包含它们的外部类, 还可以访问局部变量。不过, 那些局部变量必须事实上为 final。这说明, 它们一旦赋值就绝不会改变。
  28. 将局部内部类的使用再深人一步。 假如只创建这个类的一个对象,就不必命名了。这种类被称为匿名内部类(anonymous inner class)。
  29. 由于构造器的名字必须与类名相同, 而匿名类没有类名, 所以, 匿名类不能有构造器。取而代之的是,将构造器参数传递给超类 ( superclass) 构造器。尤其是在内部类实现接口的时候, 不能有任何构造参数。

代理

  1. 利用代理可以在运行时创建一个实现了一组给定接口的新类 : 这种功能只有在编译时无法确定需要实现哪个接口时才有必要使用。
  2. 何时使用代理

假设有一个表示接口的 Class 对象(有可能只包含一个接口,) 它的确切类型在编译时无法知道。这确实有些难度。要想构造一个实现这些接口的类, 就需要使用 newlnstance 方法或反射找出这个类的构造器。但是, 不能实例化一个接口,需要在程序处于运行状态时定义一个新类。
为了解决这个问题, 有些程序将会生成代码;将这些代码放置在一个文件中;调用编译器;然后再加载结果类文件。很自然, 这样做的速度会比较慢,并且需要将编译器与程序放在一起。而代理机制则是一种更好的解决方案。代理类可以在运行时创建全新的类。这样的代理类能够实现指定的接口。尤其是,它具有下列方法:

  • 指定接口所需要的全部方法。
  • Object 类中的全部方法, 例如, toString、 equals 等。
  1. 不能在运行时定义这些方法的新代码。而是要提供一个调用处理器(invocation handler)。调用处理器是实现了 InvocationHandler 接口的类对象。在这个接口中只有一个方法:
    Object invoke(Object proxy, Method method, Object[] args)
    无论何时调用代理对象的方法, 调用处理器的 invoke 方法都会被调用, 并向其传递Method 对象和原始的调用参数。 调用处理器必须给出处理调用的方式。
  2. 创建代理对象:
    要想创建一个代理对象, 需要使用 Proxy 类的 newProxylnstance 方法。 这个方法有三个参数:
    • 一个类加栽器(class loader)。作为 Java 安全模型的一部分, 对于系统类和从因特网
      上下载下来的类,可以使用不同的类加载器。目前,用 null 表示使用默认的类加载器。
    • 一个 Class 对象数组, 每个元素都是需要实现的接口。
    • 一个调用处理器。