24隋心所欲’s Blog

技术改变世界


  • 首页

  • 归档

Java 异常(四):如何使用异常

发表于 2019-01-10 | 分类于 《Java 编程的逻辑》
  1. 异常的适用情况是?
    答:

    • 异常应该且仅用于异常情况,即异常不能代替正常的条件判断。
    • 真正出现异常的时候,应该抛出异常,而不是返回特殊值。
  2. 异常的来源大概是?
    答:

    • 用户:用户的输入有问题。
    • 程序员:编程错误。
    • 第三方:泛指其他情况,如 I/O 错误、网络、数据库、第三方服务等。
    • 每种异常都应该进行适当的处理。
  3. 异常处理的目标是?
    答:

    • 处理的目标可以分为恢复和报告。恢复是指通过程序自动解决问题。报告的最终对象可能是用户,即程序使用者,也可能是系统运维人员或程序员。报告的目的也是为了恢复,但这个恢复经常需要人的参与。
    • 对用户,如果用户输入不对,可以提示用户具体哪里输入不对;如果是编程错误,可以提示用户系统错误、建议联系客服;如果是第三方连接问题,可以提示用户稍后重试。
    • 程序都不应该假定第三方是可靠的,应该有容错机制。
  4. 异常处理的一般逻辑是?
    答:

    • 如果自己知道怎么处理异常,就进行处理;如果可以通过程序自动解决,就自动解决;如果异常可以被自己解决,就不需要再向上报告。
    • 如果自己不能完全解决,就应该向上报告。如果自己有额外信息可以提供,有助于分析和解决问题,就应该提供,可以以原异常为 cause 重新抛出一个异常。
    • 总有一层代码需要为异常负责。可能是知道如何处理该异常的代码,可能是面对用户的代码,也可能是是主程序;如果异常不能自动解决,对于用户,应该根据异常信息提供用户能理解和对用户有帮助的信息;对运维和开发人员,则应该输出详细的异常链和异常栈到日志。
  5. 为什么要有异常处理机制?
    答:

    • 在没有异常机制的情况下,唯一的退出机制就是 return,判断是否异常的方法就是返回值。
    • 程序的正常逻辑和异常逻辑混杂在一起,代码的可读性和可维护性变差。
    • 程序员经常偷懒,假装异常不会发生。
  6. 异常处理机制的好处?
    答:处理异常情况的代码大大减少,代码的可读性、可靠性、可维护性都得到提高,具体表现在:

    • 程序的正常逻辑与异常逻辑可以相分离。
    • 异常情况可以集中进行处理。
    • 异常还可以自动向上传递,不再需要每层方法都进行处理。
    • 异常也不再可能被自动忽略。

Java 异常(三):异常处理

发表于 2019-01-10 | 分类于 《Java 编程的逻辑》
  1. 怎样理解异常处理中的 catch 匹配?
    答:

    • catch 语句可以有多条,每条对应一种异常类型。
    • 自 Java 7 开始支持一种新的语法,多个异常之间可以用 “ | ” 操作符。
  2. 怎样理解重新抛出(throw)异常?
    答:在 catch 块内处理完后,可以重新抛出异常,异常可以是原来的,也可以是新建的。

  3. 为什么要重新抛出(throw)异常?
    答:因为当前代码不能够完全处理该异常,需要调用者进一步处理。

  4. 为什么要抛出(throw)一个新的异常?
    答:

    • 当然是因为当前异常不太合适,不合适可能是信息不够,需要补充一些新信息。
    • 还可能是过于细节,不便于调用者理解和使用。
    • 如果调用者对细节感兴趣,还可以通过 getCause() 方法获取到原始异常。
  5. 怎样理解异常机制中的 finally ?
    答:finally 内的代码不管有无异常发生,都会执行。具体来说:

    • 如果没有异常发生,在 try 内的代码执行结束后执行。
    • 如果有异常发生且被 catch 捕获,在 catch 内的代码执行结束后执行。
    • 如果有异常发生但没被捕获,则在异常被抛给上层之前执行。
  6. 使用 finally 的场景?
    答:finally 一般用于释放资源,如数据库连接、文件流等。

  7. finally 语句的执行细节?
    答:

    • 如果在 try 或 catch 语句内有 return 语句,则 return 语句在 finally 语句执行结束后才执行,但 finally 并不能改变返回值,即 finally 中对返回值的修改不会被返回。(这点也是挺奇葩的)
    • 如果在 finally 语句中也有 return 语句,那么 try 或 catch 内的 return 会丢失,实际会返回 finally 中的返回值。
    • finally 中有 return 不仅会覆盖 try 和 catch 内的返回值,还会掩盖 try 和 catch 内的异常,就像异常没有发生一样。也就是在异常抛出之前执行了 finally 里的 return 语句。
    • 如果 finally 中抛出了异常,则原异常也会被掩盖。
    • 一般而言,为避免混淆,应该避免在 finally 中使用 return 语句或者抛出异常,如果调用的其他代码可能抛出异常,则应该捕获异常并进行处理,即异常嵌套。
  8. 怎样理解 try-with-resources?
    答:对于一些使用资源的场景,比如文件和数据库连接,典型的使用流程是首先打开资源,最后在 finally 语句中调用资源的关闭方法。针对这种场景,Java 7 开始支持一种新的语法,称之为 try-with-resources。

    • 这种语法针对实现了 java.lang.AutoCloseable 接口的对象,该接口的定义为:

      1
      2
      3
      public interface AutoCloseable {
      void close() throws Exception;
      }
* 没有 `try-with-resources` 时,使用形式如下:

    
1
2
3
4
5
6
7
8
public static void useResource() throws Exception {
AutoCloseable r = new FileInputStream("hello"); // 创建资源
try {
// 使用资源
} finally {
r.close();
}
}
* 使用 `try-with-resources` 语法时,形式如下:
1
2
3
4
5
public static void useResource() throws Exception {
try(AutoCloseable r = new FileInputStream("hello")) { // 创建资源
// 使用资源
}
}
资源 `r` 的声明和初始化放在 `try` 语句内,不用再调用 `finally`,在语句执行完 `try` 语句后,会**自动调用资源的 `close()` 方法**,对程序员更加友好。 * **资源可以定义多个,以分号分隔**。在 **Java 9** 之前,资源必须声明和初始化在 `try` 语句块内,Java 9 去除了这个限制,资源可以在 `try` 语句外被声明和初始化,但必须是 final 的或者是事实上 `final` 的(即虽然没有声明为 `final` 但也没有被重新赋值)。
  1. 怎样理解 throws 关键字?
    答:

    • 用于声明一个方法可能抛出的异常。
    • throws 跟在方法的括号后面,可以声明多个异常,以逗号分隔。
    • 这个声明的含义是:这个方法内可能抛出这些异常,且没有对这些异常进行处理,至少没有处理完,调用者必须进行处理。
    • 这个声明没有说明具体什么情况会抛出异常,作为一个良好的实践,应该将这些信息用注释的方式进行说明,这样调用者才能更好地处理异常。
  2. throws 关键字的细节?
    答:

    • 对于未受检异常,是不要求使用 throws 进行声明的,但对于受检异常,则必须进行声明。换句话说,对于受检异常,如果没有声明,则不能抛出。
    • 对于受检异常,不可以抛出而不声明,但可以声明抛出但实际不抛出。这主要用于在父类方法中声明,父类方法内可能没有抛出,但子类重写方法后可能就抛出了。子类不能抛出父类方法中没有声明的受检异常,所以就将所有可能抛出的异常都写到父类上了。
    • 如果一个方法内调用了另一个声明抛出受检异常的方法,则必须处理这些受检异常。处理方式既可以是 catch,也可以是继续使用 throws。
  3. 未受检异常和受检异常的区别?
    答:受检异常必须出现在 throws 语句中,调用者必须处理,Java 编译器会强制这一点,而未受检异常则没有这个要求。

  4. 为什么要有这个区分?我们自定义异常的时候使用使用受检还是未受检异常?
    答:

    • 对于这个问题,业界有各种各种的观点和争论,没有特别一致的结论。
    • 目前一种更被认同的观点是:Java 中对受检异常和未受检异常的区分是没有太大意义的,可以统一使用未受检异常来代替。
    • 无论是受检异常还是未受检异常,无论是否出现在 throws 声明中,都应该在合适的地方以适当的方式进行处理。
    • 观点本身不重要,重要的是一致性:一个项目中,应该对如何使用异常达成一致,并按照约定使用。

Java 异常(二):异常类

发表于 2019-01-09 | 分类于 《Java 编程的逻辑》
  1. 怎样理解 Throwable ?
    答:

    • Throwable 是所有异常类的父类。
    • Throwable 类有两个主要参数:一个是 message,表示异常信息;另一个是 cause,表示触发该异常的其他异常。
    • 异常可以形成一个异常链,上层的异常由底层的异常触发,cause 表示底层异常。
    • Throwable 还有一个 public 方法用于设置 cause:Throwable initCause(Throwable cause)。Throwable 的某些子类没有带 cause 参数的构造方法,就可以通过这个方法来设置,这个方法最多只能被调用一次。
    • 在所有构造方法的内部,都有一句重要的方法调用:fillInStackTrace(),它会将异常栈信息保存下来,这是我们能看到异常栈的关键。
  2. Throwable 中常用的用于获取异常信息的方法有哪些?
    答:

    • void printStackTrace(),打印异常栈信息到标准错误输出流。
    • void printStackTrace(PrintStream s) / void printStackTrace(PrintWriter s),打印栈信息到指定的流。
    • String getMessage(),获取设置的异常 message。
    • Throwable getCause(),获取异常的 cause。
    • StackTraceElement[] getStackTrace(),获取异常栈每一层的信息,每个 StackTraceElement 包括文件名、类名、方法名、行号等信息。
  3. 异常类的体系是怎样的?
    答:

    • Throwable 是所有异常类的基类,它有两个子类:Error 和 Exception。
    • Error 表示系统错误或资源耗尽,即不可控的内部原因,由 Java 系统自己使用,应用程序不应抛出和处理。常见的子类有:VirtualMachineError、OutOfMemoryError、StackOverflowError
    • Exception 表示应用程序错误,即不可控的外部原因,它有很多子类,应用程序也可以通过继承 Exception 或其子类创建自定义异常。常见的子类有:IOException、RuntimeException、SQLException。
  4. 怎么理解未受检异常和受检异常?
    答:

    • 未受检异常包括:RuntimeException 及其子类和Error 及其子类。
    • 受检异常包括:Exception 及其除了 RuntimeException 之外的其他子类。
    • RuntimeException 比较特殊,它的名字有点误导,因为其他异常也是运行时产生的,它表示的实际含义就是未受检异常。
    • 受检和未受检的区别在于 Java 如何处理这两种异常。对于受检异常,Java 会强制要求程序员进行处理,否则会有编译错误,而对于未受检异常则没有这个要求。有点类似于编译时和运行时的区别。
  5. 为什么定义这么多不同的异常类?
    答:

    • 这么多不同的异常类其实并没有比 Throwable 这个基类多多少属性和方法,大部分类在继承父类后只是定义了几个构造方法,这些构造方法也只是调用了父类的构造方法,并没有额外的操作。
    • 之所以定义这么多不同的异常类主要是为了名字不同。异常类的名字本身就代表了异常的关键信息,无论是抛出异常还是捕获异常,使用合适的名字都有助于代码的可读性和可维护性。
  6. 怎样自定义异常?
    答:

    • 一般是继承 Exception 或者它的某个子类。
    • 如果继承的父类是 RuntimeException 或它的某个子类,那么此时自定义异常也是未受检异常。
    • 如果继承的父类是 Exception 或它的除了 RuntimeException 之外的其他子类,那么此时自定义的异常是受检异常。

Java 异常(一):初识异常

发表于 2019-01-09 | 分类于 《Java 编程的逻辑》
  1. 发生异常的原因?
    答:

    • 不可控的内部原因,比如内存不够了、磁盘满了。
    • 不可控的外部原因,比如网络连接出现问题。
    • 更多的是程序编写错误,比如空指针异常等。
  2. 怎样理解异常这个概念?
    答:

    • 这些非正常情况在 Java 中统一被认为是异常,Java 使用异常机制来统一处理。
    • 异常是相对于 return 的一种退出机制,可以由系统触发,也可以由程序通过 throw 语句触发。
    • 异常可以通过 try/catch 语句进行捕获并处理,如果没有捕获,则会触发默认处理机制,即输出异常栈信息并退出程序。
    • 异常有不同的类型。
  3. 发生空指针异常时,具体发生了什么?
    答:

    • JVM 发现对象引用为 null 时,没有办法继续执行了,这时就启用异常处理机制,首先创建一个异常对象,这里是类 NullPointerException 的对象,然后查找看谁能处理这个异常。如果没有代码可以处理这个异常,那么 Java 会启用默认处理机制,即打印异常栈信息到屏幕,并退出程序。
    • 异常栈信息就包括了从异常发生点到最上层调用者的轨迹,还包括行号,可以说,这个栈信息是分析异常最为重要的信息。
    • Java 的默认异常处理机制是退出程序,异常发生点后的代码都不会执行。
  4. throw 关键字的含义?
    答:throw 的意思是抛出异常,它会触发 Java 的异常处理机制。在比如说空指针异常中,我们没有看到 throw 的代码,可以认为 throw 是由 Java 虚拟机自己实现的。

  5. 对比一下 throw 关键字和 return 关键字?
    答:

    • return 代表正常退出,throw 代表异常退出。
    • return 的返回位置是确定的,就是上一级调用者;throw 后执行哪行代码则经常是不确定的,由异常处理机制动态确定。
  6. 异常处理机制的流程?
    答:

    • 异常处理机制会从当前函数开始查看谁“捕获”了这个异常,当前函数没有就查看上一层,直到主函数,如果主函数也没有,就使用默认机制,即输出异常栈信息并退出。
    • “捕获”是指使用 try/catch 关键字,try 后面的花括号 {} 包含可能抛出异常的代码。捕获异常后,程序就不会异常退出了,但 try 语句内异常点之后的其他代码就不会执行了,执行完 catch 内的语句后,程序会继续执行 catch 花括号外的代码。

Java 类的扩展(四):枚举的本质

发表于 2019-01-09 | 分类于 《Java 编程的逻辑》
  1. 怎样理解枚举这个概念?
    答:

    • 枚举是一种取值有限的特殊的数据。
    • 使用关键字 enum 定义枚举,值一般是大写字母,多个值之间以逗号分隔。
    • 枚举类型可以定义为一个单独的文件,也可以定义在其他类内部。
  2. 枚举的细节?
    答:

    • 枚举变量的 toString() 方法返回其字面值,所有枚举类型也都有一个 name() 方法,返回值与 toString() 一样。
    • 枚举变量可以使用 equals 和 == 进行比较,结果是一样的。
    • 枚举值是有顺序的,可以比较大小。枚举类型都有一个方法 int ordinal(),表示枚举值在声明时的顺序,从 0 开始。
    • 枚举类型都实现了 Java API 中的 Comparable 接口,都可以通过方法 compareTo() 与其他枚举值进行比较,其实就是比较 ordinal 的大小。
    • 枚举变量可以用于和其他类型变量一样的地方,如方法参数、类变量、实例变量等。枚举还可以用于 switch 语句。在 switch 语句中,枚举值会被转换为其对应的 ordinal 值。
    • 在 switch 语句内部的 case 值部分,枚举值不能带枚举类型前缀。
    • 枚举类型都有一个静态的 valueOf(String) 方法,返回字符串对应的枚举值。
    • 枚举类型也都有一个静态的 values() 方法,返回一个包括所有枚举值的数组,顺序与声明时的一致。
    • Java 是从 Java 5 才开始支持枚举的,在此之前,一般是在类中定义静态整型变量来实现类似功能。
    • 枚举类型本质上也是类,但由于编译器自动做了很多事情,因此它的使用更为简洁、安全和方便。
  3. 枚举的好处?
    答:

    • 定义枚举的语法更为简洁,可读性更强。
    • 枚举更为安全。一个枚举类型的变量,它的值要么为 null,要么为枚举值之一,不可能为其他值;但使用整型变量,它的值就没有办法强制,值可能就是无效的。
    • 枚举类型自带很多便利的方法,易于使用,对程序员更友好。
  4. 枚举是怎么实现的?
    答:

    • 枚举类型实际上会被 Java 编译器转换为一个对应的类,这个类继承了 Java API 中的 java.lang.Enum 类。
    • Enum 类有 name 和 ordinal 两个实例变量,在构造方法中需要传递,name()、toString()、ordinal()、compareTo()、equals() 方法都是由 Enum 类根据其实例变量 name 和 ordinal 实现的。
    • values() 和 valueOf() 方法是编译器给每个枚举类型自动添加的。
  5. 枚举的典型应用场景?
    答:

    • 实际中枚举经常会有关联的实例变量和方法,每个枚举值可能有关联的缩写和中文名称。
    • 需要注意的是,枚举值的定义需要放在最上面,以逗号分隔,枚举值写完之后,要以分号结尾,然后才能写其他代码。
    • 每个枚举值经常有一个关联的标识符(id),通常用 int 整数表示,使用整数可以节约存储空间,减少网络传输。
  6. 为什么不能使用枚举自带的 ordinal 值表示枚举值的 id?
    答:使用 ordinal 值并不是一个好的选择,因为 ordinal 值会随着枚举值在定义中的位置变化而变化,但一般来说,我们希望 id 值和枚举值的关系保持不变,尤其是表示枚举值的 id 已经保存在了很多地方的时候。

  7. 枚举还有哪些高级用法?
    答:

    • 每个枚举值可以有关联的类定义体。
    • 枚举类型可以声明抽象方法,每个枚举值中可以实现该方法,也可以重写枚举类型的其他方法。
    • 枚举可以实现接口,也可以在接口中定义枚举。

Java 类的扩展(三):内部类的本质

发表于 2019-01-07 | 分类于 《Java 编程的逻辑》
  1. 怎样理解内部类?
    答:

    • 一个类放在另一个类的内部,称为内部类。
    • 内部类只是 Java 编译器的概念,对于 JVM 而言,它是不知道内部类这回事的,每个内部类最后都会被编译为一个独立的类,生成一个独立的字节码文件。
    • 内部类可以方便地访问外部类的私有变量。
    • 内部类本质上都会被转换为独立的类,但一般而言,它们可以实现更好的封装,代码实现上也更简洁。
  2. 内部类的好处?
    答:

    • 可以实现对外部完全隐藏。
    • 更好的封装性。
    • 写法上也更简洁。
  3. 内部类的分类?
    答:根据定义的位置和方式不同,主要有 4 种内部类:

    • 静态内部类。
    • 成员内部类。
    • 方法内部类。
    • 匿名内部类。
  4. 怎样理解静态内部类?
    答:

    • 语法上,静态内部类除了位置放在其他类内部外,它与一个独立的类差别不大,可以有静态变量、静态方法、成员方法、成员变量、构造方法等。
    • 静态内部类与外部类的联系不大,它可以访问外部类的静态变量和方法,但不能访问实例变量和方法。在类内部,可以直接使用内部静态类。
    • public 静态内部类可以被外部使用,语法是:外部类.静态内部类。
  5. 静态内部类是怎样实现的?
    答:

    • 静态内部类会被编译为 Outer$Inner 的形式。
    • 静态内部类访问外部类的私有静态变量的实现是:Java 自动为外部类生成一个非私有静态方法 access$0,这个方法返回这个私有静态变量。
  6. 静态内部类的使用场景?
    答:如果它与外部类关系密切,且不依赖于外部类实例,则可以考虑定义为静态内部类。

  7. 试举出几个 Java API 中使用静态内部类的例子?
    答:

    • Integer 类内部有一个私有静态内部类 IntegerCache,用于支持整数的自动装箱。
    • 表示链表的 LinkedList 类内部有一个私有静态内部类 Node,表示链表中的每个节点。
    • Character 类内部有一个 public 静态内部类 UnicodeBlock,用于表示一个 Unicode Block。
  8. 怎样理解成员内部类?
    答:

    • 成员内部类没有 static 修饰符。
    • 与静态内部类不同,除了静态变量和方法,成员内部类还可以直接访问外部类的实例变量和方法。
    • 如果和外部类有重名,成员内部类还可以通过 外部类.this.xxx 的方式引用外部类的实例变量和方法,如 Outer.this.action()。
  9. 怎样创建成员内部类对象?
    答:

    • 与静态内部类不同,成员内部类对象总是与一个外部类对象相连,在外部使用时,因为不是静态,所以它不能直接通过 new Outer.Inner() 的方式创建对象,而是要先创建一个外部类对象。
    • 语法:外部类对象.new 内部类(),如 outer.new Inner()。
  10. 成员内部类、方法内部类和匿名内部类中都不可以定义静态变量和方法,Java 为什么要有这个规定?
    答:可以这么理解,这些内部类是与外部实例相连的,不应独立使用,而静态变量和方法作为类的属性和方法,一般是独立使用的,在内部类中意义不大,而如果内部类确实需要静态变量和方法,那么也可以挪到外部类中。说白了,静态与非静态,一个是类的属性,一个是对象的属性,二者是泾渭分明的。

  11. 成员内部类是怎么实现的?
    答:

    • 生成两个类:一个是 Outer,一个是 Outer$Inner。
    • 外部类一般会相应生成两个非私有静态方法:access$0 用于访问变量,access$1用于访问方法。
  12. 成员内部类有哪些应用场景?
    答:

    • 如果内部类与外部类关系密切,需要访问外部类的实例变量或方法,则可以考虑定义为成员内部类。
    • 外部类的一些方法的返回值可能是某个接口,为了返回这个接口,外部类方法可能使用内部类实现这个接口,这个内部类可以被设为 private,对外完全隐藏。
  13. 试举出 Java API 中使用成员内部类的例子?
    答:类 LinkedList 中,它的两个方法 listIterator 和 descendingIterator 的返回值都是接口 Iterator,调用者可以通过 Iterator 接口对链表遍历, listIterator 和 descendingIterator 内部分别使用了成员内部类 ListItr 和 DescendingIterator,这两个内部类都实现了接口 Iterator。

  14. 怎样理解方法内部类?
    答:

    • 内部类还可以定义在一个方法体中,即为方法内部类。
    • 方法内部类只能在定义的方法内被使用。
    • 如果方法是实例方法,则除了静态变量和方法,方法内部类还可以直接访问外部类的实例变量和方法;如果方法是静态方法,则方法内部类只能访问外部类的静态变量和方法。
    • 方法内部类还可以直接访问方法的参数和方法中的局部变量,不过,这些变量必须被声明为 final。
    • 方法内部类可以用成员内部类代替,至于方法参数,也可以作为参数传递给成员内部类。不过,如果类只在某个方法内被使用,使用方法内部类,可以实现更好的封装。
    • 实际使用中较少,感觉主要是 Java 的一个语法糖,也许 JDK 源码中应用得较多些。
  15. 为什么方法内部类访问外部方法中的参数和局部变量时,这些变量必须被声明为 final?
    答:

    • 因为实际上,方法内部类操作的并不是外部的变量,而是它自己的实例变量,只是这些变量的值和外部一样,对这些变量赋值,并不会改变外部的值,为避免混淆,左移干脆强制规定必须声明为 final。
    • 如果的确需要修改外部的变量,那么可以将变量改为只含该变量的数组,修改数组中的值。
  16. 怎样理解匿名内部类?
    答:

    • 匿名内部类没有单独的类定义,它在创建对象的同时定义类。
    • 匿名内部类只能被使用一次,用来创建一个对象。
    • 匿名内部类没有名字,没有构造方法,但可以根据参数列表,调用对应的父类构造方法。
    • 匿名内部类可以定义实例变量和方法,可以有初始化代码块,初始化代码块可以起到构造方法的作用,只是构造方法可以有多个,而初始化代码块只能有一份。
    • 因为没有构造方法,它自己无法接受参数,如果必须要参数,则应该使用其他内部类。
    • 与方法内部类一样,匿名内部类也可以访问外部类的所有变量和方法,可以访问方法中的 final 参数和 final 局部变量。
  17. 匿名内部类是怎么实现的?
    答:每个匿名内部类也都被生成一个独立的类,只是类的名字以外部类加数字编号,没有有意义的名字。

  18. 匿名内部类的使用场景?
    答:匿名内部类能做的,方法内部类都能做。但如果对象只会创建一次,且不需要构造方法来接收参数,则可以使用匿名内部类,写法上也更简洁。

  19. 怎么理解回调这个概念?
    答:

    • 回调即回过头来调用,是相对于一般的正向调用而言的。
    • 匿名内部类是实现回调接口的一种简便方式。

Java 类的扩展(二):抽象类

发表于 2019-01-06 | 分类于 《Java 编程的逻辑》
  1. 怎么理解抽象类和抽象方法?
    答:

    • 抽象类:就是抽象的类,抽象类没有直接对应的对象,表达的是抽象的概念,一般是具体类的比较上层的父类。
    • 抽象方法:只有子类才知道如何实现的方法。
    • 关键字:abstract。
  2. 抽象类的细节?
    答:

    • 定义了抽象方法的类必须被声明为抽象类,但抽象类可以没有抽象方法。
    • 抽象类和具体类一样,可以定义具体方法、实例变量等。
    • 抽象类和具体类的核心区别是:抽象类不能创建对象。
    • 抽象类不能创建对象,要创建对象,必须使用它的具体子类。
    • 一个类在继承抽象类后,必须实现抽象类中定义的所有抽象方法。
    • 与接口类似,抽象类虽然不能使用 new,但可以声明抽象类的变量,引用抽象类具体子类的对象。
  3. 为什么需要抽象类、为什么不能定义一个空方法表示抽象的含义?
    答:

    • 引入抽象方法和抽象类,是 Java 提供的一种语法工具,对于一些类和方法,引导使用者正确使用它们。虽然从语法上抽象类不是必需的,但它能使程序更清晰,可以减少误用。
    • 使用抽象方法而非空方法体,子类就知道它必须要实现该方法,而不可能忽略,若忽略 Java 编译器会提示错误。
    • 使用抽象类,类的使用者创建对象的时候,就知道必须要使用它的某个具体子类,而不可能误用不完整的父类。
    • 每个人都可能犯错,减少错误不能只依赖人的优秀素质,还需要一些机制,使得一个普通人都容易把事情做对,而难以把事情做错。抽象类就是 Java 提供的这样的一种减少犯错的机制。
  4. 抽象类和接口的区别?
    答:

    • 接口中不能定义实例变量,抽象类可以。
    • 抽象类和接口是配合而非替代关系,接口声明能力,抽象类提供默认实现,实现全部或部分方法。
    • 一个接口经常有一个对应的抽象类。

Java 类的扩展(一):接口的本质

发表于 2019-01-06 | 分类于 《Java 编程的逻辑》
  1. 接口的本质是什么?
    答:接口是一种能力,很多时候反映的是对象以及对对象操作的本质。

  2. 定义接口的格式?
    答:

    • 用 interface 这个关键字声明接口,修饰符一般都是 public。
    • 没有定义方法体,Java 8 之前,接口内不能实现方法。
    • 接口方法不需要加修饰符,加与不加都相当于 public abstract。
    • 定义一个接口本身并没有做什么,也没有太大的用处,它还需要至少两个参与者:一个需要实现接口,另一个使用接口。
    • 类可以实现接口,表示类的对象具有接口所表示的能力。
  3. 使用接口的格式?
    答:

    • 接口不能 new,不能直接创建一个接口对象,对象只能通过类来创建。
    • 可以声明接口类型的变量,引用实现了接口的类对象。
  4. 针对接口编程的好处是?
    答:代码复用,降低耦合,提高灵活性。

  5. 接口的几个小细节?
    答:

    • 接口中的变量。接口中可以定义变量,修饰符是 public static final,但这个修饰符是可选的,即使不写,也是 public static final,通过接口名.变量名访问变量。
    • 接口的继承。接口也可以继承,和类一样的是,关键字也是 extends;和类不同的是,接口可以有多个父接口,即 public interface IChild extends IBase1, IBase2 {} 的写法是合法的。
    • 类的继承与接口。类的继承与接口可以共存,需要注意的是关键字 extends 要放在 implements 之前。
    • instanceof。与类一样,接口也可以使用 instanceof 关键字,用来判断一个对象是否实现了某接口。
  6. 怎么理解使用组合和接口替代继承?
    答:

    • 继承至少有两个好处:一个是复用代码;另一个是利用多态和动态绑定统一处理多种不同子类的对象。
    • 使用组合替代继承,可以复用代码,但不能统一处理。
    • 使用接口替代继承,针对接口编程,可以实现统一处理不同类型的对象,但接口没有代码实现,无法复用代码。
    • 将组合和接口结合起来替代继承,就既可以统一处理,又可以复用代码了。
  7. Java 8 和 Java 9 对接口做了哪些增强?
    答:

    • 在 Java 8 之前,接口中的方法都是抽象方法,都没有实现体。
    • Java 8 允许在接口中定义两类新方法:静态方法和默认方法,它们有实现体。
    • 示例:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      public interface IDemo {
      void hello();
      public static void test() {
      System.out.println("hello");
      }
      default void hi() {
      System.out.println("hi");
      }
      }
    • 静态方法 test() 可以通过 IDemo.test() 调用。
      * 默认方法用关键字 default 修饰。默认方法有默认的实现,实现类可以改变它的默认实现,也可以不改变。

    • 在 Java 8 中,静态方法和默认方法都必须是 public 的,Java 9 去除了这个限制,它们都可以是 private 的,引入 private 方法主要是为了方便多个静态或默认方法复用代码。
  8. Java 8 引入默认方法的原因是?
    答:

    • 引入默认方法主要是函数式数据处理的需求,是为了便于给接口增加功能。
    • 在没有默认方法之前,Java 是很难给接口增加功能的。比如 List 接口,因为有太多非 Java JDK 控制的代码实现了该接口,如果给接口增加一个方法,则那些接口的实现就无法在新版 Java 上运行,必须改写代码实现新的方法,这显然是无法接受的。
    • 函数式数据处理需要给一些接口增加一些新的方法,所以就有了默认方法的概念,接口增加了新方法,而接口现有的实现类也不需要必须实现。
  9. 接口增加默认方法的示例?
    答:

    • List 接口增加了 sort 默认方法,定义为:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      default void sort(Comparator<? super E> c) {
      Object[] a = this.toArray();
      Arrays.sort(a, (Comparator) c);
      ListIterator<E> i = this.listIterator();
      for(Object e : a) {
      i.next();
      i.set((E) e);
      }

      }
    • Collection 接口增加了 stream 默认方法,其定义为:

      1
      2
      3
      default Stream<E> stream() {
      return StreamSupport.stream(spliterator(), false);
      }

Java 类的继承(四):为什么说继承是把双刃剑

发表于 2019-01-06 | 分类于 《Java 编程的逻辑》
  1. 继承的强大表现在哪?
    答:

    • 继承广泛应用于各种 Java API、框架和类库之中。
    • 一方面它们内部大量使用继承,另一方面它们设计了良好的框架结构,提供了大量基类和基础公共代码。
    • 使用者可以使用继承,重写适当方法进行定制,就可以简单方便地实现强大的功能。
  2. 继承为什么会有破坏力?
    答:

    • 继承可能破坏封装,而封装可以说是程序设计的第一原则。
    • 继承可能没有反映出 is-a 的关系。
  3. 怎样理解封装?
    答:

    • 封装就是隐藏实现细节,提供简化接口。
    • 使用者只需要关注怎么用而不需要关注内部是怎么实现的。实现细节可以随时修改,而不影响使用者。
    • 方法是封装,类也是封装。通过封装,才能在更高的层次上考虑和解决问题。
    • 可以说,封装是程序设计的第一原则,没有封装,代码之间会到处存在着实现细节的依赖,则构建和维护复杂的程序是难以想象的,可阅读性和可维护性就会变得很差。
  4. 怎样理解继承可能破坏封装?
    答:

    • 因为子类和父类之间可能存在着实现细节的依赖。子类在继承父类的时候,往往不得不关注父类的实现细节,而父类在修改其内部实现的时候,如果不考虑子类,也往往会影响到子类。
    • 如果子类不知道父类方法的实现细节,它就不能正确地进行扩展。
    • 子类和父类之间是细节依赖,子类扩展父类,但仅仅知道父类能做什么还是不够的,还需要知道父类是怎么做的,而父类的实现细节也不能随便修改,否则可能影响子类。
    • 更具体地说,子类需要知道父类的可重写方法之间的依赖关系,而且这个依赖关系,父类不能随意改变。但即使这个依赖关系不变,封装还是可能因为细节修改而被破坏。
    • 父类不能随意增加公开方法,因为给父类增加就是给所有子类增加,而子类可能必须要重写该方法才能确保方法的正确性。
    • 总结:对于子类而言,通过继承实现是没有安全保障的,因为父类修改内部实现细节,它的功能就可能会被破坏;对于父类而言,让子类继承和重写方法,就可能丧失随意修改内部实现的自由。
  5. 为什么说继承可能没有反映 is-a 关系?
    答:

    • 继承关系是设计用来反映 is-a 关系的,子类是父类的一种,子类对象也属于父类,父类的属性和行为也适用于子类。
    • 但现实中,设计完全符合 is-a 关系的继承关系是困难的。在 is-a 关系中,重写方法时,子类不应该改变父类预期的行为,但是这是没有办法约束的。
    • 继承是应该被当做 is-a 关系使用的,但是,Java 并没有办法约束。父类有的属性和行为,子类并不一定都适用,子类还可以重写方法,实现与父类预期完全不一样的行为。
    • 对于通过父类引用操作子类对象的程序而言,它是把对象当作父类对象来看待的,期望对象符合父类中声明的属性和行为。如果不符合,则有可能会造成混乱,代码可维护性变差。
  6. 继承既强大又有破坏性,那怎么办?
    答:

    • 避免使用继承。
    • 正确使用继承。
  7. 怎样避免使用继承?
    答:

    • 使用 final 关键字。
    • 优先使用组合而非继承。
    • 使用接口。
  8. final 关键字对于继承关系的影响?
    答:

    • final 方法不能被重写,final 类不能被继承。
    • 给方法加 final 修饰符,父类就保留了随意修改这个方法内部实现的自由,使用这个方法的程序也可以确保其行为是符合父类声明的。
    • 给类加 final 修饰符,父类就保留了随意修改这个类实现的自由,使用者也可以放心地使用它,而不用担心一个父类引用的变量,实际指向的却是一个完全不符合预期行为的子类对象。
  9. 使用组合而非继承的优劣是?
    答:

    • 使用组合可以抵挡父类变化对子类的影响,从而保护子类,应该优先使用组合。
    • 但组合的问题是,子类对象不能当作基类对象来统一处理了。解决方法是使用接口。
  10. 怎样正确使用继承呢?
    答:使用继承大概主要有三种场景:

    • 基类是别人写的,我们写子类。

      基类主要是 Java API、其他框架或类库中的类,在这种情况下,我们主要通过扩展基类实现自定义行为,这种情况下需要注意的是:

      • 重写方法不要改变预期的行为。
      • 阅读文档说明,理解可重写方法的实现机制,尤其是方法之间的依赖关系。
      • 在基类修改的情况下,阅读其修改说明,相应修改子类。
    • 我们写基类,别人可能写子类。

      我们写基类给别人用,在这种情况下,需要注意的是:

      • 使用继承反映真正的 is-a 关系,只将真正公共的部分放到基类。
      • 对不希望被重写的公开方法添加 final 修饰符。
      • 写文档,说明可重写方法的实现机制,为子类提供指导,告诉子类应该如何重写。
      • 在基类修改可能影响子类时,写修改说明。
    • 基类、子类都是我们写的。

      我们既写基类也写子类,关于基类,注意事项和第 2 种场景类似。关于子类,注意事项和第 1 种场景类似。不过程序都由我们控制,要求可以适当放松一些。

Java 类的继承(三):继承实现的基本原理

发表于 2018-12-30 | 分类于 《Java 编程的逻辑》
  1. 请写出下面代码的输出结果?

    • Base 类

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      public class Base {

      public static int s;
      private int a;

      static {
      System.out.println("基类静态代码块,s: " + s);
      s = 1;
      }

      {
      System.out.println("基类实例代码块,a: " + a);
      a = 1;
      }

      public Base() {
      System.out.println("基类构造方法,a: " + a);
      a = 2;
      }

      protected void step() {
      System.out.println("base s: " + s + ", a: " + a);
      }

      public void action() {
      System.out.println("start");
      step();
      System.out.println("end");
      }
      }
* Child 类

    
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class Child extends Base {

public static int s;
private int a;

static {
System.out.println("子类静态代码块,s: " + s);
s = 10;
}

{
System.out.println("子类实例代码块,a: " + a);
a = 10;
}

public Child() {
System.out.println("子类构造方法,a: " + a);
a = 20;
}

protected void step() {
System.out.println("child s: " + s + ", a: " + a);
}
}
* main 方法
1
2
3
4
5
6
7
8
9
10
11
12
13
public static void main(String[] args) {

System.out.println("---- new Child()");
Child c = new Child();
System.out.println("\n---- c.action()");
c.action();
Base b = c;
System.out.println("\n---- b.action()");
b.action();
System.out.println("\n---- b.s " + b.s);
System.out.println("\n---- c.s: " + c.s);

}
答: * 输出结果如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
---- new Child()
基类静态代码块,s: 0
子类静态代码块,s: 0
基类实例代码块,a: 0
基类构造方法,a: 1
子类实例代码块,a: 0
子类构造方法, a: 10

---- c.action()
start
child s: 10, a: 20
end

---- b.action()
start
child s: 10, a: 20
end

---- b.s: 1
---- c.s: 10
* 总结:
1
2
3
基静态 -> 子静态
基实例代码块 -> 基构造
子实例代码块 -> 子构造
  1. 类的加载什么意思?怎么理解这个概念?
    答:

    • 类的加载是指将类的相关信息加载进内存。
    • 在 Java 中,类是动态加载的,当第一次使用这个类的时候才会加载。
    • 加载一个类时,会查看其父类是否已加载,如果没有,则会加载其父类。
  2. 一个类的信息包括哪些部分?
    答:

    • 类变量(静态变量)
    • 类初始化代码
    • 类方法(静态方法)
    • 实例变量
    • 实例初始化代码
    • 实例方法
    • 父类信息引用
  3. 类初始化代码包括哪些部分?
    答:

    • 定义静态变量时的赋值语句
    • 静态初始化代码块
  4. 实例初始化代码包括哪些部分?
    答:

    • 定义实例变量时的赋值语句
    • 实例初始化代码块
    • 构造方法
  5. 类加载过程是怎样的?
    答:

    • 分配内存保存类的信息
    • 给类变量赋默认值
    • 加载父类
    • 设置父子关系
    • 执行类初始化代码
  6. 怎样理解方法区这个概念?
    答:存放类的信息的一个内存区,Object 类肯定会存在于方法区。

  7. 创建对象的过程是怎样的?
    答:

    • 分配内存
    • 对所有实例变量赋默认值
    • 执行实例初始化代码

      注意:

      • 分配的内存包括本类和所有父类的实例变量,但不包括任何静态变量。
      • 实例初始化代码的执行从父类开始,再执行子类的。但在任何类执行初始化代码之前,所有实例变量都已设置完默认值。
      • 每个对象除了保存类的实例变量之外,还保存着实际类信息的引用。
  8. 动态绑定机制中方法的调用过程是怎样的?
    答:动态绑定实现的机制就是根据对象的实际类型查找要执行的方法,子类型中找不到的时候再查找父类。

  9. 虚方法表的含义及作用?
    答:

    • 含义:在类加载的时候为每个类创建一个表,这个表叫做虚方法表,这个表记录该类的对象所有动态绑定的方法(包括父类的方法)及其地址,但一个方法只有一条记录,子类重写了父类方法后只会保留子类的。
    • 作用:优化方法调用的效率。如果继承的层次比较深,要调用的方法位于比较上层的父类,如果不用虚方法表的话则调用的效率是比较低的,因为每次调用都要进行很多次查找。使用虚方法表的话,当通过对象动态绑定方法的时候,只需要查找这个表就可以了,不需要挨个查找每个父类。
  10. 对变量的访问是静态绑定的,这句话对吗?
    答:对。

  11. 对象访问类变量(静态变量)的实现机制是?
    答:通过对象访问类变量,系统会转换为类名直接访问静态变量。

1…678…10
24隋心所欲

24隋心所欲

code for die

99 日志
9 分类
124 标签
© 2019 24隋心所欲
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4