[Think In Java]基础拾遗2 - 多态、反射、异常、字符串

时间:2021-11-23 17:26:20

目录

第八章 多态
第十四章 类型信息
第十二章 通过异常处理错误
第十三章 字符串

第八章 多态

1. 前期绑定 & 后期绑定

绑定是指将方法调用同一个方法主体关联起来的这么一个过程。如果在程序执行前进行绑定,就称为前期绑定。(C中所有的方法调用都是前期绑定)

后期绑定就是指在运行时根据对象的类型进行绑定。后期绑定也叫动态绑定/运行时绑定。

一种语言要想实现后期绑定,就必须具有某种机制,以便于在运行时能判断对象的类型,从而调用恰当的方法。不同的语言有不同的实现机制,不管怎么样都必须在对象中安置某种类型信息。(比如在C++中是通过在对象中安插一个指向虚函数表的指针来保存该对象的类型信息)。由此可见,运行时类型信息很重要!那java中是如何实现在运行时来获取对象的类型信息的呢?

java中除了static方法和final方法(private方法属于final方法)之外,其他所有的方法都是后期绑定的。声明为final的方法会告诉编译器“关闭”动态绑定。

2. 不会产生多态的情况

(1)子类覆盖父类的private方法

(2)访问字段(用一个指向子类的父类引用访问字段是不会产生多态的),并且这个访问是在编译时解析。

(3)静态方法

3. 如果覆盖了父类的方法,特指资源清理类方法,记得先释放子类部分的资源,然后super调用父类的方法以释放对象的父类部分资源。

4. RTTI

在运行期间对类型进行检查的行为称作运行时类型识别

第十四章 类型信息

运行时类型信息使得我们可以在运行时发现和使用类型信息。

1. java是如何让我们在运行时识别对象和类的信息的呢?

两种方式:

(1)传统的RTTI:它假定我们在编译时已经知道了所有的类型。(也就是说:在编译的时候就知道在某个类继承体系中有哪些类型了。)

(2)反射机制:它允许我们在运行时发现和使用类的信息。

2. class对象

要理解RTTI在java中的工作原理,首先必须知道类型信息在运行时是如何表示的。这项工作是由称为class对象的特殊对象完成的,它包含了与该类有关的信息。

[个人理解:编程语言中的所谓对象,无非就是内存中的一块区域,那么class对象就是这样的一块内存,它里面保存的是类相关的信息。]

类是程序的一部分,每个类都有一个class对象。

[个人理解:一个类有哪些方法,有哪些域,所有的这些信息都应该保存在内存中的一个区域,每个类在内存中都应该有这样的一块区域来保存这样的信息,而每一个这样的一块区域被称为class对象。]

3. java特有的动态加载

java中所有的类都是在对其第一次使用时,动态加载到JVM中的。所谓动态加载是指:java程序在它开始运行之前并非被完全加载,其各个部分是指在必需时才加载的。这一点与许多传统语言都不同。C++是静态加载语言。

4. 为什么要先有class对象?

class对象保存了对象的类型信息,如果没有class对象,就不知道对象应该占据多大的内存,在new对象的时候就不知道该分配多大的空间。所以在创建对象实例之前JVM一定会保证已经加载了对应的类。一旦一个类的class对象在内存中被创建好了,它就被用来创建这个类的所有对象。

5. 获取某个类A的class对象的引用

(1)public static Class<?> forName(String name) throws ClassNotFoundException

返回与带有给定字符串名的类或接口相关联的 Class 对象。

这种方式适用于没有对象实例的情况。

(2)调用A对象实例的getClass()方法

这种方式适用于有对象实例的情况。

(3)类字面常量 "A.class"

这种方式简单、安全,在编译时就会受到检查。

6. 为了使用一个类而做的准备(类的加载过程):

(1)加载:在内存中创建一个class对象。

(2)链接:验证字节码、为静态域分配存储空间、解析对其他类的所有引用

(3)初始化:初始化静态域的存储空间

7. 在java中哪些情况下需要RTTI呢?

(1)传统的类型转换:由RTTI确保类型转换的正确性,若执行了一个错误的类型转换,会抛出ClassCastException异常。

(2)查询Class对象相关的操作。

(3)instanceof:用法是 ”对象 instanceof 类型“ ,可以使用Class类的实例方法boolean isInstance(Object obj)判定指定的 Object 是否与此 Class 所表示的对象赋值兼容。此方法是 Java 语言 instanceof 运算符的动态等效方法。即对于类A来说,下面的两个if语句等效:

A a = new A();
if(a instanceof A)
{
} if(A.class.isInstance(a))
{
}

8. instanceof 和 class的等价性

用instanceof保持了类型的概念,表示“你是这个类吗,或者你是这个类的派生类吗?”

用==比较实际的Class对象就没有考虑继承——它或者是这个确切的类型,或者不是。

9 RTTI和反射的区别和联系

相同点:RTTI和反射都是一种让我们在运行时可以识别对象的类型信息的一种机制。

不同点:

RTTI:由编译器在编译时打开和检查*.class文件。

反射机制:由JVM在运行时打开和检查*.class文件。

10. 个人对反射和RTTI的理解

(1)RTTI:

在编译的时候,编译器javac干的事情就是将java源文件转换成.class文件,每个具体的类可以还是引用其他类,在这个阶段javac编译器就会验证对某个类的调用是否合法之类的,如果在源程序中调用了某个类没有的方法,此时编译器就会报错的。在整个编译阶段,编译器必须要知道某个类的.class文件,否则不知道该类有哪些方法,就不能验证调用是否合法。这样的话在编译阶段,编译器就已经知道了各个类的类型,当然在运行的时候也就能提供给我们在运行时查询某个对象的类型了。

   Class clazz1 = Class.forName("rtti.Apple");    //在编译阶段编译器不会检查和打开字符串"rtti.Apple"指定的类对应的.class文件,而是在运行期间由JVM加载。
 Fruit fruit = new Bnana();    //编译阶段编译器会去查找Fruit类和Apple类各自对应的.class文件,以确定在给定对象上的方法调用是否合法。
fruit.setWeight(10);
     Class clazz2 = fruit.getClass();
System.out.println(clazz2.getName());

(2)反射:

由于没有了.class文件,就不能new对象,不能调方法,肿么办呢?我们仔细思考一下,在java中当我们获得了一个类后不外乎有如下这些操作:
(1) 创建对象(Constructor)
(2)调用方法(Method)
(3)访问/修改对象的某个字段(Field)
并且我们知道每个类都是由域,方法,构造器等元素组成的。所以java在reflect类库中提供了Field、Method以及Constructor等类型来表示每个类的域,方法,构造器等元素,于是在没有class文件无法直接new对象的情况下,我们只好面对这些组件编程咯。以下是使用这些组件编程的典型流程:

Class clazz = Class.forName("aa");    //获取class对象的引用
Object obj = clazz.newInstance(); //创建对象 for(Field f:clazz.getFields()){
f.setLong(obj, 200); //修改域
}
for(Method m:clazz.getMethods()){
m.invoke(obj, null); //调用方法
}

很明显,在反射机制下,JVM在运行时才会去获取响应的.class文件。

【认识误区纠正】: 以前提到反射机制就想到Class,脑海中渐渐形成了"反射就是Class,Class就是反射"的错误观念。应该讲反射是一种机制,在java中它是由Class以及java.lang.reflect类库提供支持的!也就是说Class只是支持反射机制的一个类而已,不代表反射就是Class,何况在RTTI下也会涉及到Class呢。

总结:Java对反射机制提供了很多支持,使得能够创建一个在编译时完全未知的对象,并调用此对象的方法。

11. 动态代理

Java中的动态代理可以动态地创建代理并动态地处理对所代理方法的调用。

调用静态方法java.lang.reflect.Proxy.newProxyInstance()可以创建动态代理。

第十二章 通过异常处理错误

1. 异常处理理论上有两种基本模型

(1)终止模型

在这种模型中,将假设错误非常关键,以至于程序无法返回到异常发生的地方继续执行。一旦异常被抛出,就表明错误已无法挽回,也不能回来继续执行。java支持这种模型。

(2)恢复模型

异常处理程序的工作是修复错误,然后才重新尝试调用出问题的方法,并认为第二次能成功。java中要想实现该模型,可以把try块放在while循环里,这样就不断地进入try块,直到得到满意的结果。

比如:

class TimeoutException extends Exception {

}

public class Retry{

    public static void main(String[] args) {
int tryTimes = 5;
while (true) {
try {
connect("www.google.com"); } catch (TimeoutException e) {
System.out.println("TimeoutException");
} finally {
if (--tryTimes == 0)
break; // out of "while"
}
}
System.out.println("exit main...");
} private static void connect(String addr) throws TimeoutException {
try {
System.out.println("conneting " + addr + " ...");
Thread.sleep(1000);
throw new TimeoutException();
} catch (InterruptedException e) { }
}
}
/*
输出结果:
conneting www.google.com ...
TimeoutException
conneting www.google.com ...
TimeoutException
conneting www.google.com ...
TimeoutException
conneting www.google.com ...
TimeoutException
conneting www.google.com ...
TimeoutException
exit main...
*/

2. 异常执行流程

例子:

 public class ExceptionTest {

     static void fun() throws Exception
{
System.out.println("ExceptionTest.fun()");
throw new Exception("MyException");
} public static void main(String[] args) { try {
fun();
System.out.println("business");
} catch (Exception e) {
System.out.println("int catch");
}finally{
System.out.println("in finally");
}
System.out.println("exiting ...");
}
}

结果:

ExceptionTest.fun()
int catch
in finally
exiting ...

3. checked异常

在编译时被强制检查的异常被称为被检查的异常(checked异常)。对于这种异常,如果在方法体中throw了这种异常,而方法签名中并没有声明的话,编译器会报错。

4. 重新抛出异常

(1)重新抛出以前的异常

在catch中不能直接throw e,这样以后调用printStackTrace()显示的将是原来异常抛出点的信息,而并非重新抛出点的信息。要想更新这个信息,可以调用fillInStackTrace()方法,它是通过把当前调用栈信息填入原来那个异常对象而建立的。throw (Exception)e.fillStackTrace(); 调用fillStackTrace()的那一行就成了异常的新发生地了。

(2)重新抛出另外一种异常

这样做的话,异常的新发生地就是重新抛出的那一行。也就是说不必按照(1)中那么做了。

例子:

public class Rethrowing {
public static void f() throws Exception {
System.out.println("originating the exception in f()");
throw new Exception("thrown from f()");
}
public static void g() throws Exception {
try {
f();
} catch(Exception e) {
System.out.println("Inside g(),e.printStackTrace()");
e.printStackTrace(System.out);
throw e;
}
}
public static void h() throws Exception {
try {
f();
} catch(Exception e) {
System.out.println("Inside h(),e.printStackTrace()");
e.printStackTrace(System.out);
throw (Exception)e.fillInStackTrace();
}
}
public static void main(String[] args) {
try {
g();
} catch(Exception e) {
System.out.println("main: printStackTrace()");
e.printStackTrace(System.out);
}
try {
h();
} catch(Exception e) {
System.out.println("main: printStackTrace()");
e.printStackTrace(System.out);
}
}
}

输出结果:

originating the exception in f()
Inside g(),e.printStackTrace()
java.lang.Exception: thrown from f()
at Rethrowing.f(Rethrowing.java:7)
at Rethrowing.g(Rethrowing.java:11)
at Rethrowing.main(Rethrowing.java:29)
main: printStackTrace()
java.lang.Exception: thrown from f()
at Rethrowing.f(Rethrowing.java:7)
at Rethrowing.g(Rethrowing.java:11)
at Rethrowing.main(Rethrowing.java:29)
originating the exception in f()
Inside h(),e.printStackTrace()
java.lang.Exception: thrown from f()
at Rethrowing.f(Rethrowing.java:7)
at Rethrowing.h(Rethrowing.java:20)
at Rethrowing.main(Rethrowing.java:35)
main: printStackTrace()
java.lang.Exception: thrown from f()
at Rethrowing.h(Rethrowing.java:24)
at Rethrowing.main(Rethrowing.java:35)

5. 异常链

有时需要捕获一个异常后抛出另一个异常,并且希望将原始异常的信息保存下来,这被称为异常链。

(1)从JDK1.4后,Throwable的子类在构造器中可以接受一个case参数,Throwable(Throwable cause),这个参数用来表示原始异常。这样即使在当前位置创建并抛出了新的异常,也能通过这个异常链追踪到异常最初发生的位置。

(2)只有三种基本的异常类提供了待cause参数的构造器(Exception,Error,RuntimeException)。若要把其他类型异常链接其他,应该用Throwable的initCause(Throwable cause)方法而不是构造器。

Throwable 内部有一个字段:private Throwable cause = this; //用于指示该Throwable 是否是由其他Throwable导致抛出的。

上述(1),(2)两种方式无外乎是设置该字段的值而已。

6. RuntimeException

RuntimeException 是那些可能在 Java 虚拟机正常运行期间抛出的异常的超类。可能在执行方法期间抛出但未被捕获的 RuntimeException 的任何子类都无需在 throws 子句中进行声明。这被称为UnChecked异常。

RuntimeException代表的是编程错误。

第十三章 字符串

1. 不可变String

String对象是不可变的。String类中每一个看起来会修改String值的方法,实际上都是创建了一个全新的String对象。

2. StringBuilder 和 StringBuffer

StringBuilder 是javase5引入的,之前是StringBuffer,StringBuffer是线程安全的。

3. System.out.format类似于C语言中的printf,还有一个格式化类java.util.Formatter专门负责格式化。