《Java编程思想》反射——类型信息笔记

package type_information;

import java.util.stream.*;

abstract class Shape {
    void draw() { System.out.println(this + ".draw()"); }
    @Override
    public abstract String toString();
}

class Circle extends Shape {
    @Override
    public String toString() { return "Circle"; }
}

class Square extends Shape {
    @Override
    public String toString() { return "Square"; }

   
   
    

}

class Triangle extends Shape {
    @Override
    public String toString() { return "Triangle"; }
}

public class Shapes {
    public static void main(String[] args) {
       Stream.of(
                new Circle(), new Square(), new Triangle())
               .forEach(Shape::draw);
        Shape shape=new Square();
       // shape.draw2();  //这一个是在子类下创建的方法,向上转型后不可以使用
    }
}
对象向上转型后 在子类创建的父类没有的方法不能被使用。子类覆盖了父类的方法向上转型后用的是子类的方法

基基类中包含 draw() 方法,它通过传递 this 参数传递给 System.out.println(),toString() 声明为 abstract,以此强制继承者覆盖该方法,并防止对 Shape 的实例化,如果某个对象出现在字符串表达式中(涉及"+"和字符串对象的表达式),toString() 方法就会被自动调用
这个例子中,在把 Shape 对象放入 Stream 中时就会进行向上转型(隐式),但在向上转型的时候也丢失了这些对象的具体类型。对 stream 而言,它们只是 Shape 对象。
另外在这个例子中,类型转换并不彻底:Object 被转型为 Shape ,而不是 CircleSquare 或者 Triangle。这是因为目前我们只能确保这个 Stream<Shape> 保存的都是 Shape

  • 编译期,stream 和 Java 泛型系统确保放入 stream 的都是 Shape 对象(Shape 子类的对象也可视为 Shape 的对象),否则编译器会报错;
  • 运行时,自动类型转换确保了从 stream 中取出的对象都是 Shape 类型。

接下来就是多态机制的事了,Shape 对象实际执行什么样的代码,是由引用所指向的具体对象(CircleSquare 或者 Triangle)决定的。这也符合我们编写代码的一般需求,通常,我们希望大部分代码尽可能少了解对象的具体类型,而是只与对象家族中的一个通用表示打交道(本例中即为 Shape)。这样,代码会更容易写,更易读和维护;设计也更容易实现,更易于理解和修改。所以多态是面向对象的基本目标。

但是,有时你会碰到一些编程问题,在这些问题中如果你能知道某个泛化引用的具体类型,就可以把问题轻松解决。例如,假设我们允许用户将某些几何形状高亮显示,现在希望找到屏幕上所有高亮显示的三角形;或者,我们现在需要旋转所有图形,但是想跳过圆形(因为圆形旋转没有意义)。这时我们就希望知道Stream里边的形状具体是什么类型,而 Java 实际上也满足了我们的这种需求。使用 RTTI,我们可以查询某个 Shape 引用所指向对象的确切类型,然后选择或者剔除特例。

Class 对象

每当我们编写并且编译了一个新类,就会产生一个 Class 对象(更恰当的说,是被保存在一个同名的 .class 文件中)。为了生成这个类的对象,Java 虚拟机 (JVM) 先会调用"类加载器"子系统把这个类加载到内存中。
所有的类都是第一次使用时动态加载到 JVM 中的,当程序创建第一个对类的静态成员的引用时,就会加载这个类。

其实构造器也是类的静态方法,虽然构造器前面并没有 static 关键字。所以,使用 new 操作符创建类的新对象,这个操作也算作对类的静态成员引用。

forName("包名+类名")Class 类的一个静态方法,我们可以使用 forName() 根据目标类的类名(String)得到该类的 Class 对象,如果 Class.forName() 找不到要加载的类,它就会抛出异常 ClassNotFoundException
传递给forName() 的字符串必须使用类的全限定名(包含包名)。
无论何时,只要你想在运行时使用类型信息,就必须先得到那个 Class 对象的引用
Class.forName()使用该方法你不需要先持有这个类型 的对象
如果你已经拥有了目标类的对象,那就可以通过调用 getClass() 方法来获取 Class 引用了,这个方法来自根类 Object,它将返回表示该对象实际类型的 Class 对象的引用
使用getName() 来产生完整类名,
使用 getSimpleName() 产生不带包名的类名,
getCanonicalName() 也是产生完整类名(除内部类和数组外,对大部分类产生的结果与 getName() 相同)
isInterface() 用于判断某个 Class 对象代表的是否为一个接口。
因此,通过 Class 对象,你可以得到关于该类型的所有信息。
Class.getInterfaces() 方法返回的是存放 Class 对象的数组,里面的 Class 对象表示的是那个类实现的接口。
getSuperclass() 方法来得到父类的 Class 对象

obj = up.newInstance();虚拟构造器可以让你在不知道一个类的确切类型的时候,创建这个类的对象,up 只是一个 Class 对象的引用,在编译期并不知道这个引用会指向哪个类的 Class 对象。当你创建新实例时,会得到一个 Object 引用,但是这个引用指向的是 Toy 对象。当然,由于得到的是 Object 引用,目前你只能给它发送 Object 对象能够接受的调用。

类字面常量

生成类对象引用的方法
① c = Class.forName(“type_information.FancyToy”); 需要抛出ClassNotFoundException
②c=FancyToy.class; 类字面常量它在编译时就会受到检查(因此不必放在 try 语句块中

注意,有一点很有趣:当使用 .class 来创建对 Class 对象的引用时,不会自动地初始化该 Class 对象。为了使用类而做的准备工作实际包含三个步骤:

  1. 加载,这是由类加载器执行的。该步骤将查找字节码(通常在 classpath 所指定的路径中查找,但这并非是必须的),并从这些字节码中创建一个 Class 对象。

  2. 链接。在链接阶段将验证类中的字节码,为 static 字段分配存储空间,并且如果需要的话,将解析这个类创建的对其他类的所有引用。

  3. 初始化。如果该类具有超类,则先初始化超类,执行 static 初始化器和 static 初始化块。

直到第一次引用一个 static 方法(构造器隐式地是 static)或者非常量的 static 字段,才会进行类初始化。

// typeinfo/ClassInitialization.java
import java.util.*;

class Initable {
   static final int STATIC_FINAL = 47;
   static final int STATIC_FINAL2 =
       ClassInitialization.rand.nextInt(1000);
   static {
       System.out.println("Initializing Initable");
   }
}

class Initable2 {
   static int staticNonFinal = 147;
   static {
       System.out.println("Initializing Initable2");
   }
}

class Initable3 {
   static int staticNonFinal = 74;
   static {
       System.out.println("Initializing Initable3");
   }
}

public class ClassInitialization {
   public static Random rand = new Random(47);
   public static void
   main(String[] args) throws Exception {
       Class initable = Initable.class;
       System.out.println("After creating Initable ref");
       // Does not trigger initialization:
       System.out.println(Initable.STATIC_FINAL);
       // Does trigger initialization:
       System.out.println(Initable.STATIC_FINAL2);
       // Does trigger initialization:
       System.out.println(Initable2.staticNonFinal);
       Class initable3 = Class.forName("Initable3");
       System.out.println("After creating Initable3 ref");
       System.out.println(Initable3.staticNonFinal);
   }
}

输出结果:

After creating Initable ref
47
Initializing Initable
258
Initializing Initable2
147
Initializing Initable3
After creating Initable3 ref
74

从对 initable 引用的创建中可以看到,仅使用 .class 语法来获得对类对象的引用不会引发初始化。但与此相反,使用 Class.forName() 来产生 Class 引用会立即就进行初始化,如 initable3
如果一个 static final 值是“编译期常量”(如 Initable.staticFinal),那么这个值不需要对 Initable 类进行初始化就可以被读取。但是,如果只是将一个字段设置成为 staticfinal,还不足以确保这种行为。例如,对 Initable.staticFinal2 的访问将强制进行类的初始化,因为它不是一个编译期常量。
如果一个 static 字段不是 final 的,那么在对它访问时,总是要求在它被读取之前,要先进行链接(为这个字段分配存储空间)和初始化(初始化该存储空间),就像在对 Initable2.staticNonFinal 的访问中所看到的那样。

泛化的 Class 引用

Class 引用总是指向某个 Class 对象,而 Class 对象可以用于产生类的实例,并且包含可作用于这些实例的所有方法代码。它还包含该类的 static 成员,因此 Class 引用表明了它所指向对象的确切类型,而该对象便是 Class 类的一个对象。

但是,Java 设计者看准机会,将它的类型变得更具体了一些。Java 引入泛型语法之后,我们可以使用泛型对 Class 引用所指向的 Class 对象的类型进行限定。在下面的实例中,两种语法都是正确的:

// typeinfo/GenericClassReferences.java

public class GenericClassReferences {
    public static void main(String[] args) {
        Class intClass = int.class;
        Class<Integer> genericIntClass = int.class;
        genericIntClass = Integer.class; // 同一个东西
        intClass = double.class;
        // genericIntClass = double.class; // 非法
    }
}

普通的类引用不会产生警告信息。你可以看到,普通的类引用可以重新赋值指向任何其他的 Class 对象,但是使用泛型限定的类引用只能指向其声明的类型。通过使用泛型语法,我们可以让编译器强制执行额外的类型检查。

那如果我们希望稍微放松一些限制,应该怎么办呢?乍一看,下面的操作好像是可以的:

Class<Number> geenericNumberClass = int.class;

这看起来似乎是起作用的,因为 Integer 继承自 Number。但事实却是不行,因为 IntegerClass 对象并不是 NumberClass 对象的子类(这看起来可能有点诡异,我们将在泛型这一章详细讨论)。

为了在使用 Class 引用时放松限制,我们使用了通配符,它是 Java 泛型中的一部分。通配符就是 ?,表示“任何事物”。因此,我们可以在上例的普通 Class 引用中添加通配符,并产生相同的结果:

// typeinfo/WildcardClassReferences.java

public class WildcardClassReferences {
    public static void main(String[] args) {
        Class<?> intClass = int.class;
        intClass = double.class;
    }
}

使用 Class<?> 比单纯使用 Class 要好,虽然它们是等价的,并且单纯使用 Class 不会产生编译器警告信息。使用 Class<?> 的好处是它表示你并非是碰巧或者由于疏忽才使用了一个非具体的类引用,而是特意为之。

为了创建一个限定指向某种类型或其子类的 Class 引用,我们需要将通配符与 extends 关键字配合使用,创建一个范围限定。这与仅仅声明 Class<Number> 不同,现在做如下声明:

// typeinfo/BoundedClassReferences.java

public class BoundedClassReferences {
    public static void main(String[] args) {
        Class<? extends Number> bounded = int.class;
        bounded = double.class;
        bounded = Number.class;
        // Or anything else derived from Number.
    }
}

Class 引用添加泛型语法的原因只是为了提供编译期类型检查,因此如果你操作有误,稍后就会发现这点

如果你手头的是超类

// typeinfo/toys/GenericToyTest.java
// 测试 Class 类
// {java typeinfo.toys.GenericToyTest}
package typeinfo.toys;

public class GenericToyTest {
    public static void
    main(String[] args) throws Exception {
        Class<FancyToy> ftClass = FancyToy.class;
        // Produces exact type:
        FancyToy fancyToy = ftClass.newInstance();
        Class<? super FancyToy> up =
            ftClass.getSuperclass();
        // This won't compile:
        // Class<Toy> up2 = ftClass.getSuperclass();
        // Only produces Object:
        Object obj = up.newInstance();
    }
}
Class 本身是一个类,而不是容器,不知道为什么可以使用泛型Class<? super FancyToy> 	

cast() 方法

Java 中还有用于 Class 引用的转型语法,即 cast() 方法:

class Building {}
class House extends Building {}

public class ClassCasts {
    public static void main(String[] args) {
        Building b = new House();
        Class<House> houseType = House.class;
        House h = houseType.cast(b);
        h = (House)b; // ... 或者这样做.
    }
}

cast() 方法接受参数对象,并将其类型转换为 Class 引用的类型。

就是强制类型转换,没啥用

类型转换检测

直到现在,我们已知的 RTTI 类型包括:

  1. 传统的类型转换,如 “(Shape)”,由 RTTI 确保转换的正确性,如果执行了一个错误的类型转换,就会抛出一个 ClassCastException 异常。

  2. 代表对象类型的 Class 对象. 通过查询 Class 对象可以获取运行时所需的信息.
    RTTI 在 Java 中还有第三种形式,那就是关键字 instanceof。它返回一个布尔值,告诉我们对象是不是某个特定类型的实例,可以用提问的方式使用它,就像这个样子:

if(x instanceof Dog)
    ((Dog)x).bark();

在将 x 的类型转换为 Dog 之前,if 语句会先检查 x 是否是 Dog 类型的对象。进行向下转型前,如果没有其他信息可以告诉你这个对象是什么类型,那么使用 instanceof 是非常重要的,否则会得到一个 ClassCastException 异常。
instanceof 有一个严格的限制:只可以将它与命名类型进行比较,而不能与 Class 对象作比较
System.out.println(new Dog() instanceof Cat);
System.out.println(new Dog() instanceof Cat.Class); 错误

类的等价比较

当你查询类型信息时,需要注意:instanceof 的形式(即 instanceofisInstance() ,这两者产生的结果相同) 和 与 Class 对象直接比较 这两者间存在重要区别。下面的例子展示了这种区别:

// typeinfo/FamilyVsExactType.java
// instanceof 与 class 的差别
// {java typeinfo.FamilyVsExactType}
package typeinfo;

class Base {}
class Derived extends Base {}

public class FamilyVsExactType {
    static void test(Object x) {
        System.out.println(
              "Testing x of type " + x.getClass());
        System.out.println(
              "x instanceof Base " + (x instanceof Base));
        System.out.println(
              "x instanceof Derived " + (x instanceof Derived));
        System.out.println(
              "Base.isInstance(x) " + Base.class.isInstance(x));
        System.out.println(
              "Derived.isInstance(x) " +
              Derived.class.isInstance(x));
        System.out.println(
              "x.getClass() == Base.class " +
              (x.getClass() == Base.class));
        System.out.println(
              "x.getClass() == Derived.class " +
              (x.getClass() == Derived.class));
        System.out.println(
              "x.getClass().equals(Base.class)) "+
              (x.getClass().equals(Base.class)));
        System.out.println(
              "x.getClass().equals(Derived.class)) " +
              (x.getClass().equals(Derived.class)));
    }

    public static void main(String[] args) {
        test(new Base());
        test(new Derived());
        Base b=new Derived();
        System.out.println(b instanceof Base);
        System.out.println(b instanceof Derived);  //即使向上转型 本质上还是   
                           //Derived
                               
      
    }
}

输出结果:

Testing x of type class typeinfo.Base
x instanceof Base true
x instanceof Derived false
Base.isInstance(x) true
Derived.isInstance(x) false
x.getClass() == Base.class true
x.getClass() == Derived.class false
x.getClass().equals(Base.class)) true
x.getClass().equals(Derived.class)) false
Testing x of type class typeinfo.Derived
x instanceof Base true
x instanceof Derived true
Base.isInstance(x) true
Derived.isInstance(x) true
x.getClass() == Base.class false
x.getClass() == Derived.class true
x.getClass().equals(Base.class)) false
x.getClass().equals(Derived.class)) true

test() 方法使用两种形式的 instanceof 对其参数执行类型检查。然后,它获取 Class 引用,并使用 ==equals() 测试 Class 对象的相等性。令人放心的是,instanceofisInstance() 产生的结果相同, equals()== 产生的结果也相同。但测试本身得出了不同的结论。与类型的概念一致,instanceof 说的是“你是这个类,还是从这个类派生的类?”。而如果使用 == 比较实际的Class 对象,则与继承无关 —— 它要么是确切的类型,要么不是。

反射:运行时类信息

如果你不知道对象的确切类型,RTTI 会告诉你。但是,有一个限制:必须在编译时知道类型,才能使用 RTTI 检测它,并对信息做一些有用的事情。换句话说,编译器必须知道你使用的所有类。
起初,这看起来并没有那么大的限制,但是假设你引用了一个不在程序空间中的对象。实际上,该对象的类在编译时甚至对程序都不可用。也许你从磁盘文件或网络连接中获得了大量的字节,并被告知这些字节代表一个类。由于这个类在编译器为你的程序生成代码后很长时间才会出现,你如何使用这样的类?
Class 支持反射的概念, java.lang.reflect 库中包含类 FieldMethodConstructor(每一个都实现了 Member 接口)。这些类型的对象由 JVM 在运行时创建,以表示未知类中的对应成员
重要的是要意识到反射没有什么魔力。当你使用反射与未知类型的对象交互时,JVM 将查看该对象,并看到它属于特定的类(就像普通的 RTTI)。在对其执行任何操作之前,必须加载 Class 对象。 因此,该特定类型的 .class 文件必须在本地计算机上或通过网络对 JVM 仍然可用。因此,RTTI 和反射的真正区别在于,使用 RTTI 时,编译器在编译时会打开并检查 .class 文件。换句话说,你可以用“正常”的方式调用一个对象的所有方法。通过反射,.class 文件在编译时不可用;它由运行时环境打开并检查。

接口和类型

package type_information.interfacea;

public interface A {
    void f();
}

创建一个实现A接口只有包权限的类,设置一个静态方法,返回A向上转型

package type_information.packageaccess;
import type_information.interfacea.*;
class C implements A {
    @Override
    public void f() {
        System.out.println("public C.f()");
    }

    public void g() {
        System.out.println("public C.g()");
    }

    void u() {
        System.out.println("package C.u()");
    }

    protected void v() {
        System.out.println("protected C.v()");
    }

    private void w() {
        System.out.println("private C.w()");
    }
}

public class HiddenC {
    public static A makeA() {
        return new C();
    }
}

即使你从 makeA() 返回的是 C 类型,你在包的外部仍旧不能使用 A 之外的任何方法,因为你不能在包的外部命名 C。
放在不同包里 即使import了 C类存在的那个包 也不能转型
现在如果你试着将其向下转型为 C,则将被禁止,因为在包的外部没有任何 C 类型可用
但是通过反射,仍然可以调用所有方法

package type_information;
import  type_information.interfacea.*;
import  type_information.packageaccess.*;
import java.lang.reflect.*;

public class HiddenImplementation {
    public static void main(String[] args) throws Exception {
        A a = HiddenC.makeA();
        a.f();
        System.out.println(a.getClass().getName());
        // Compile error: cannot find symbol 'C':
        /* if(a instanceof C) {   //这条语句是false
            C c = (C)a;  //强制转型失败
            c.g();  
        } */
        // Oops! Reflection still allows us to call g():
        callHiddenMethod(a, "g");
        // And even less accessible methods!
        callHiddenMethod(a, "u");
        callHiddenMethod(a, "v");
        callHiddenMethod(a, "w");
    }

    static void callHiddenMethod(Object a, String methodName) throws Exception {
        Method g = a.getClass().getDeclaredMethod(methodName);
        g.setAccessible(true);
        g.invoke(a);
    }
}

那如果把接口实现为一个私有内部类,又会怎么样呢?下面展示了这种情况:

// typeinfo/InnerImplementation.java
// Private inner classes can't hide from reflection

import typeinfo.interfacea.*;

class InnerA {
    private static class C implements A {
        public void f() {
            System.out.println("public C.f()");
        }

        public void g() {
            System.out.println("public C.g()");
        }

        void u() {
            System.out.println("package C.u()");
        }

        protected void v() {
            System.out.println("protected C.v()");
        }

        private void w() {
            System.out.println("private C.w()");
        }
    }

    public static A makeA() {
        return new C();
    }
}

public class InnerImplementation {
    public static void
    main(String[] args) throws Exception {
        A a = InnerA.makeA();
        a.f();
        System.out.println(a.getClass().getName());
        // Reflection still gets into the private class:
        HiddenImplementation.callHiddenMethod(a, "g");
        HiddenImplementation.callHiddenMethod(a, "u");
        HiddenImplementation.callHiddenMethod(a, "v");
        HiddenImplementation.callHiddenMethod(a, "w");
    }
}

输出结果:

public C.f()
InnerA$C
public C.g()
package C.u()
protected C.v()
private C.w()

这里对反射仍然没有任何东西可以隐藏。那么如果是匿名类呢?

// typeinfo/AnonymousImplementation.java
// Anonymous inner classes can't hide from reflection

import typeinfo.interfacea.*;

class AnonymousA {
    public static A makeA() {
        return new A() {
            public void f() {
                System.out.println("public C.f()");
            }

            public void g() {
                System.out.println("public C.g()");
            }

            void u() {
                System.out.println("package C.u()");
            }

            protected void v() {
                System.out.println("protected C.v()");
            }

            private void w() {
                System.out.println("private C.w()");
            }
        };
    }
}

public class AnonymousImplementation {
    public static void
    main(String[] args) throws Exception {
        A a = AnonymousA.makeA();
        a.f();
        System.out.println(a.getClass().getName());
        // Reflection still gets into the anonymous class:
        HiddenImplementation.callHiddenMethod(a, "g");
        HiddenImplementation.callHiddenMethod(a, "u");
        HiddenImplementation.callHiddenMethod(a, "v");
        HiddenImplementation.callHiddenMethod(a, "w");
    }
}

输出结果:

public C.f()
AnonymousA$1
public C.g()
package C.u()
protected C.v()
private C.w()

看起来任何方式都没法阻止反射调用那些非公共访问权限的方法。对于字段来说也是这样,即便是 private 字段:

// typeinfo/ModifyingPrivateFields.java

import java.lang.reflect.*;

class WithPrivateFinalField {
    private int i = 1;
    private final String s = "I'm totally safe";
    private String s2 = "Am I safe?";

    @Override
    public String toString() {
        return "i = " + i + ", " + s + ", " + s2;
    }
}

public class ModifyingPrivateFields {
    public static void main(String[] args) throws Exception {
        WithPrivateFinalField pf =
                new WithPrivateFinalField();
        System.out.println(pf);
        Field f = pf.getClass().getDeclaredField("i");
        f.setAccessible(true);
        System.out.println(
                "f.getInt(pf): " + f.getInt(pf));
        f.setInt(pf, 47);
        System.out.println(pf);
        f = pf.getClass().getDeclaredField("s");
        f.setAccessible(true);
        System.out.println("f.get(pf): " + f.get(pf));
        f.set(pf, "No, you're not!");
        System.out.println(pf);
        f = pf.getClass().getDeclaredField("s2");
        f.setAccessible(true);
        System.out.println("f.get(pf): " + f.get(pf));
        f.set(pf, "No, you're not!");
        System.out.println(pf);
    }
}

输出结果:

i = 1, I'm totally safe, Am I safe?
f.getInt(pf): 1
i = 47, I'm totally safe, Am I safe?
f.get(pf): I'm totally safe
i = 47, I'm totally safe, Am I safe?
f.get(pf): Am I safe?
i = 47, I'm totally safe, No, you're not!
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值