第十四章 类型信息
运行时类型信息可以使得你可以在程序运行时发现和使用类型信息。
本章主要讨论Java如何让我们在运行时识别对象和类的信息,主要有两种方式:
- “传统的RTTI”,假定我们在编译时已经知道了所有的类型;
- “反射机制”,允许在运行时发现和使用类的信息。
14.1 为什么需要RTTI(Run-Time Type Identification)
RTTI提供类型维护的信息,为多态机制提供实现基础。多态的实现主要是通过向上转型,通过泛化父类来引用子类对象。
package com.typeinfo;
import java.util.Arrays;
import java.util.List;
/**
* @author [email protected]
* @since 2018-06-06 14:52
*/
public class Shapes {
public static void main(String[] args) {
List<Shape> shapeList = Arrays.asList(new Circle(), new Square(), new Triangle());
shapeList.forEach(Shape::draw);
}
}
abstract class Shape {
void draw() {
System.out.println(this + ".draw.");
}
public abstract String toString();
}
class Circle extends Shape {
@Override
public String toString() {
return "Circle";
}
}
class Triangle extends Shape {
@Override
public String toString() {
return "Triangle";
}
}
class Square extends Shape {
@Override
public String toString() {
return "Square";
}
}
output:
Circle.draw.
Square.draw.
Triangle.draw.
Process finished with exit code 0
14.2 Class对象
RTTI的功能主要是由Class类实现的,每个类都是Class类的一个对象。所有的类都是在第一次使用时被”类加载器”动态的加载到JVM中的,当程序创建第一个类的成员引用时,便会加载这个类,这说明构造器是类的静态方法。使用new创建的类的新对象也会被当作类的静态成员,因此Java是动态加载的。首先检查类的Class对象是否加载,如果尚未加载,默认的类的加载器会查找同名的.class文件,一旦某个类的Class对象被载入内存,便会用来创建这个类的所有对象。
package com.typeinfo;
/**
* @author [email protected]
* @since 2018-06-07 17:07
*/
public class SweetShop {
public static void main(String[] args) {
System.out.println("inside main.");
new Candy();
System.out.println("after create candy");
new Candy();
System.out.println("after create candy2");
try{
Class.forName("com.typeinfo.Gum");
}catch (ClassNotFoundException e) {
System.out.println("Class Gum not found.");
}
System.out.println("after create after gum.");
new Cookie();
System.out.println("after create cookie");
}
}
class Candy {
static {
System.out.println("Loading candy.");
}
}
class Gum {
static {
System.out.println("Loading gum.");
}
}
class Cookie {
static {
System.out.println("Loading cookie.");
}
}
output:
inside main.
Loading candy.
after create candy
after create candy2
Loading gum.
after create after gum.
Loading cookie.
after create cookie
Process finished with exit code 0
从输出中可以看出,Java中的类都是在需要时才被加载的。forName()是Class类的一个静态成员,调用该方法可以返回相应类的实例。
无论何时,若要使用类型信息,必须持有Class对象的引用,Class.forName()是实现此功能的便捷方式,因为不需要再为了获取Class引用而持有该类型的引用。如果已经拥有一个类型的对象,可以使用getClass()来获取Class引用,返回表示该对象的实际类型的Class引用。
package com.typeinfo;
/**
* @author [email protected]
* @since 2018-06-07 18:22
*/
public class ToyTest {
private static void printInfo(Class cc) {
System.out.println("Class name: " + cc.getName() + " is interface? [" + cc.isInterface() + "]");
System.out.println("Simple name: " + cc.getSimpleName());
System.out.println("Canonical name: " + cc.getCanonicalName());
System.out.println("---------------");
}
public static void main(String[] args) {
Class c = null;
try {
c = Class.forName("com.typeinfo.FancyToy");
} catch (ClassNotFoundException e) {
System.out.println("class not found.");
}
assert c != null;
printInfo(c);
for (Class face : c.getInterfaces()) {
printInfo(face);
}
Class up = c.getSuperclass();
Object object = null;
try {
object = up.newInstance();
} catch (InstantiationException e) {
System.out.println("获取实例失败.");
} catch (IllegalAccessException e) {
System.out.println("无法访问.");
}
assert object != null;
printInfo(object.getClass());
}
}
interface HasBatteries {
}
interface WaterProof {
}
interface Shoots {
}
class Toy {
Toy() {
}
Toy(int i) {
}
}
class FancyToy extends Toy implements HasBatteries, WaterProof, Shoots {
FancyToy() {
super(1);
}
}
output:
```java
Class name: com.typeinfo.FancyToy is interface? [false]
Simple name: FancyToy
Canonical name: com.typeinfo.FancyToy
Class name: com.typeinfo.HasBatteries is interface? [true]
Simple name: HasBatteries
Canonical name: com.typeinfo.HasBatteries
Class name: com.typeinfo.WaterProof is interface? [true]
Simple name: WaterProof
Canonical name: com.typeinfo.WaterProof
Class name: com.typeinfo.Shoots is interface? [true]
Simple name: Shoots
Canonical name: com.typeinfo.Shoots
Class name: com.typeinfo.Toy is interface? [false]
Simple name: Toy
Canonical name: com.typeinfo.Toy
Process finished with exit code 0
在给forName()方法传递类名是必须使用全限定名。
Class类常用方法列表:
| 方法 | 描述 |
| ------------------ | ---------------------------------- |
| getName() | 获取全限定类名 |
| getSimpleName() | 获取不含包类名 |
| getCanonicalName() | 含包名的全限定名 |
| getInterfaces() | Class对象所包含的接口 |
| isInterface() | 判断Class对象是否是接口 |
| newInstance() | 虚拟构造器(该类必须含有默认构造器) |
| getSuperClass() | 间接获取Class对象基类 |
### 14.2.1 类字面常量
Java还提供了另一种方法来生成Class对象的引用,即使用类字面常量,例如
```java
FancyToy.class;
这样做更加简单、安全,因为它在编译器就会受到检查,因此不需要异常检查,同时根除了对forNam()方法的调用,因此更加高效。
类字面常量方法不仅适用于普通的类,也适用于接口,基本数据类型,数组。对于基本包装类型的Class对象。还有一个标准字段TYPE,TYPE字段是一个引用,指向对应的基本类型Class对象。
建议使用.class方法,与普通类保持一致
字面常量 | 标准TYPE字段 |
---|---|
boolean.class | Boolean.class |
char.class | Char.class |
byte.class | Byte.class |
short.class | Short.class |
int.class | Integer.class |
long.class | Long.class |
float.class | Flocat.class |
double.class | Double.class |
void.class | Void.class |
当使用.class来创建Class对象引用时,不会自动初始化该Class对象,包含三个过程的准备:
- 加载:由类加载器执行,查找字节码,为字节码创建Class对象;
- 链接:验证类中的字节码,为静态域分配空间,如果必须的话,将解析这个类创建的对其他类的所有引用;
- 初始化:如果该类具有超类,则对其进行初始化,执行静态初始化和静态初始化块。初始化被延迟到对静态方法或者非静态数据域进行首次引用时执行。
package com.typeinfo;
import java.util.Random;
/**
* @author [email protected]
* @since 2018-06-07 19:07
*/
public class ClassInitailization {
public static Random random = new Random(47);
public static void main(String[] args) throws ClassNotFoundException {
Class initable = Initable.class;
System.out.println("after create initable ref.");
System.out.println(Initable.staticFinal);
System.out.println(Initable.staticFinal2);
System.out.println(Initable2.staticNonFinal);
Class initable3 = Class.forName("com.typeinfo.Initable3");
System.out.println("after create initable3.");
System.out.println(Initable3.staticNonFinal);
}
}
class Initable {
static final int staticFinal = 47;
static final int staticFinal2 = ClassInitailization.random.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.");
}
}
output:
after create initable ref.
47
Initializing initable.
258
initializing initable2.
147
initializing initable3.
after create initable3.
74
Process finished with exit code 0
初始化有效的体现了”惰性”,从Initable的引用创建中知道使用.class创建Class引用不会引发初始化,但是为了产生引用,Class.forName()立即进行了初始化。
如果一个static final值是编译期常量,那么这个值不需要对类进行初始化就可以进行读取。但是将一个域设置为static final不足以确保这种行为,如Initable.staticFinal2的访问强制类进行了初始化。
如果一个常量是static但不是final的,那么在对它进行访问时,总是要求在对它进行读取前,先进行链接(分配存储空间)和初始化(初始化该域磁盘)。
14.2.2 泛化的Class引用
Class引用所表示的就是它所指向的类的确切类型,可以制造类的实例,包含类的静态成员和方法代码。
在Java SE5中Class引用的类型变得更加具体,这是通过允许你Class引用所指向的Class对象引用类型进行限定实现的。
package com.typeinfo;
/**
* @author [email protected]
* @since 2018-06-07 19:36
*/
public class GenericClassReference {
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<Number> c = int.class;
上述代码产生了编译错误,因为Integer Class对象不是Number Class的子类。
为了能够在使用泛化的Class引用时放宽限制,可以使用泛型通配符。
package com.typeinfo;
/**
* @author [email protected]
* @since 2018-06-07 19:46
*/
public class WildcardClassReference {
public static void main(String[] args) {
Class<? extends Number> c = int.class;
c = double.class;
}
}
上述代码没有产生任何警告信息。Class>优于普通的Class,并且两者等价。
**Class>的好处是它表示你并非碰巧或者处于疏忽而使用了一个非具体的类型。**为了创建一个Class引用,它被限定为某种类型,和extends结合,可以创建一个范围。
使用泛型可以促使编译器在编译器提供类型检查。
将泛型用于Class对象,newInstance()方法返回的将是具体的类型而不是Object。但是这种具体类型有些许限制,由具体类型的Class引用getSuperClass()返回的类型是Object类型。
package com.typeinfo;
/**
* @author [email protected]
* @since 2018-06-07 20:04
*/
public class GenericToyTest {
public static void main(String[] args) throws IllegalAccessException, InstantiationException {
Class<FancyToy> fancyToyClass = FancyToy.class;
FancyToy fancyToy = fancyToyClass.newInstance();
Class<? super FancyToy> c = fancyToyClass.getSuperclass();
// Class<Toy> toyClass = fancyToyClass.getSuperclass(); 编译错误
Object object = fancyToyClass.getSuperclass();
}
}
14.2.3 新的转型语法
Java SE5还添加了用于Class引用转型的语法,即cast()方法。cast()方法接收参数对象,并将其转型为Class引用的类型。
在Java SE5中另一个最没用的新特性就是Class.asSubclass,该方法允许你将一个类对象转为更加具体的对象类型。
package com.typeinfo;
/**
* @author [email protected]
* @since 2018-06-07 20:12
*/
public class ClassCasts {
Building building = new House();
Class<House> houseClass = House.class;
House house = houseClass.cast(building);
}
class Building{
}
class House extends Building {
}
14.3 类型转换前先做检查
迄今为止,RTTI的形式包括:
- 传统的类型转换;
- 代表对象的类型的Class对象;
- 关键字instanceof:判断一个对象是不是某个特定类型的实例。
if(x instaceof Dog) {
(Dog)x.bark();
}
instanceof有比较严格的限制,只可将其与命名类型比较,而不能与Class对象比较。
14.5 instanceof和Class的等价性
instanceof()和isInstance()方法生成的结果完全一样,equals和==也完全一样。但是instanceof保持了类型的概念,而==比较的是实际的Class对象,没有考虑继承。
package com.typeinfo;
/**
* @author [email protected]
* @since 2018-06-07 20:51
*/
public class FamilyVsExactType {
static void test(Object object) {
System.out.println("Testing object of type :" + object.getClass());
System.out.println("object instanceof Base: " + (object instanceof Base));
System.out.println("object instanceof Derived :" + (object instanceof Derived));
System.out.println("Base.isInstance(object): " + Base.class.isInstance(object));
System.out.println("Derived.isInstance(object): " + Derived.class.isInstance(object));
System.out.println("object.getClass() == Base.class(): " + (object.getClass() == Base.class));
System.out.println("object.getClass() == Derived.class(): " + (object.getClass() == Derived.class));
System.out.println("object.getClass().equals(Base.class()): " + (object.getClass().equals(Base.class)));
System.out.println("object.getClass().equals(Derived.class()): " + (object.getClass().equals(Derived.class)));
}
public static void main(String[] args) {
test(new Base());
test(new Derived());
}
}
class Base {
}
class Derived extends Base {
}
output:
Testing object of type :class com.typeinfo.Base
object instanceof Base: true
object instanceof Derived :false
Base.isInstance(object): true
Derived.isInstance(object): false
object.getClass() == Base.class(): true
object.getClass() == Derived.class(): false
object.getClass().equals(Base.class()): true
object.getClass().equals(Derived.class()): false
Testing object of type :class com.typeinfo.Derived
object instanceof Base: true
object instanceof Derived :true
Base.isInstance(object): true
Derived.isInstance(object): true
object.getClass() == Base.class(): false
object.getClass() == Derived.class(): true
object.getClass().equals(Base.class()): false
object.getClass().equals(Derived.class()): true
Process finished with exit code 0
14.6 反射:运行时的类信息
RTTI的限制:如果需要知道某个对象的确切类型,可以使用RTTI,但是有一个限制:这个类型在编译时必须是可知的。如果获取了持有一个不在程序空间内的对象的引用或者这个类在你的程序运行许久之后才出现,此时则无法使用RTTI。
为什么需要反射?
- 解决RTTI的限制;
- 希望提供在跨网络的平台上创建和运行对象的能力,即*远程方法调用(RMI)*。
Class类与Java.lang.Reflect类库对反射进行了支持,该类库包含Field、Method和Constructor等类。可以用invoke()调用与Method关联的方法,调用getField()、getMethods()和getConstructor()等方法返回表示字段、方法和构造方法的数组。
RTTI与反射之间真正的区别在于对于RTTI来说,编译器在编译时打开和检查.class文件,而对于反射机制来说,.class文件在编译时是不可获取的,所以是在运行时打开和检查.class文件。
14.6.1 类方法提取器
package com.typeinfo;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
/**
* @author [email protected]
* @since 2018-06-08 9:45
*/
public class ShowMethods {
private static String usage = "usage";
public static void main(String[] args) {
if (args.length < 1) {
System.out.println(usage);
System.exit(0);
}
try {
Class<?> c = Class.forName(args[0]);
Method[] methods = c.getMethods();
Constructor[] constructors = c.getConstructors();
if (args.length == 1) {
for (Method method : methods) {
System.out.println(method.toString());
}
for (Constructor constructor : constructors) {
System.out.println(constructor.toString());
}
} else {
for (Method method : methods) {
if (method.toString().contains(args[1])) {
System.out.println(method.toString());
}
}
for (Constructor constructor : constructors) {
if (constructor.toString().contains(args[1])) {
System.out.println(constructor.toString());
}
}
}
} catch (ClassNotFoundException e) {
System.out.println("No such class.");
}
}
}
output:
public boolean java.lang.String.equals(java.lang.Object)
public java.lang.String java.lang.String.toString()
public int java.lang.String.hashCode()
public int java.lang.String.compareTo(java.lang.String)
public int java.lang.String.compareTo(java.lang.Object)
public int java.lang.String.indexOf(java.lang.String,int)
public int java.lang.String.indexOf(java.lang.String)
public int java.lang.String.indexOf(int,int)
public int java.lang.String.indexOf(int)
public static java.lang.String java.lang.String.valueOf(int)
public static java.lang.String java.lang.String.valueOf(long)
public static java.lang.String java.lang.String.valueOf(float)
public static java.lang.String java.lang.String.valueOf(boolean)
public static java.lang.String java.lang.String.valueOf(char[])
public static java.lang.String java.lang.String.valueOf(char[],int,int)
public static java.lang.String java.lang.String.valueOf(java.lang.Object)
public static java.lang.String java.lang.String.valueOf(char)
public static java.lang.String java.lang.String.valueOf(double)
public char java.lang.String.charAt(int)
public int java.lang.String.codePointAt(int)
public int java.lang.String.codePointBefore(int)
public int java.lang.String.codePointCount(int,int)
public int java.lang.String.compareToIgnoreCase(java.lang.String)
public java.lang.String java.lang.String.concat(java.lang.String)
public boolean java.lang.String.contains(java.lang.CharSequence)
public boolean java.lang.String.contentEquals(java.lang.CharSequence)
public boolean java.lang.String.contentEquals(java.lang.StringBuffer)
public static java.lang.String java.lang.String.copyValueOf(char[])
public static java.lang.String java.lang.String.copyValueOf(char[],int,int)
public boolean java.lang.String.endsWith(java.lang.String)
public boolean java.lang.String.equalsIgnoreCase(java.lang.String)
public static java.lang.String java.lang.String.format(java.util.Locale,java.lang.String,java.lang.Object[])
public static java.lang.String java.lang.String.format(java.lang.String,java.lang.Object[])
public void java.lang.String.getBytes(int,int,byte[],int)
public byte[] java.lang.String.getBytes(java.nio.charset.Charset)
public byte[] java.lang.String.getBytes(java.lang.String) throws java.io.UnsupportedEncodingException
public byte[] java.lang.String.getBytes()
public void java.lang.String.getChars(int,int,char[],int)
public native java.lang.String java.lang.String.intern()
public boolean java.lang.String.isEmpty()
public static java.lang.String java.lang.String.join(java.lang.CharSequence,java.lang.CharSequence[])
public static java.lang.String java.lang.String.join(java.lang.CharSequence,java.lang.Iterable)
public int java.lang.String.lastIndexOf(int)
public int java.lang.String.lastIndexOf(java.lang.String)
public int java.lang.String.lastIndexOf(java.lang.String,int)
public int java.lang.String.lastIndexOf(int,int)
public int java.lang.String.length()
public boolean java.lang.String.matches(java.lang.String)
public int java.lang.String.offsetByCodePoints(int,int)
public boolean java.lang.String.regionMatches(int,java.lang.String,int,int)
public boolean java.lang.String.regionMatches(boolean,int,java.lang.String,int,int)
public java.lang.String java.lang.String.replace(char,char)
public java.lang.String java.lang.String.replace(java.lang.CharSequence,java.lang.CharSequence)
public java.lang.String java.lang.String.replaceAll(java.lang.String,java.lang.String)
public java.lang.String java.lang.String.replaceFirst(java.lang.String,java.lang.String)
public java.lang.String[] java.lang.String.split(java.lang.String)
public java.lang.String[] java.lang.String.split(java.lang.String,int)
public boolean java.lang.String.startsWith(java.lang.String,int)
public boolean java.lang.String.startsWith(java.lang.String)
public java.lang.CharSequence java.lang.String.subSequence(int,int)
public java.lang.String java.lang.String.substring(int)
public java.lang.String java.lang.String.substring(int,int)
public char[] java.lang.String.toCharArray()
public java.lang.String java.lang.String.toLowerCase(java.util.Locale)
public java.lang.String java.lang.String.toLowerCase()
public java.lang.String java.lang.String.toUpperCase()
public java.lang.String java.lang.String.toUpperCase(java.util.Locale)
public java.lang.String java.lang.String.trim()
public final void java.lang.Object.wait() throws java.lang.InterruptedException
public final void java.lang.Object.wait(long,int) throws java.lang.InterruptedException
public final native void java.lang.Object.wait(long) throws java.lang.InterruptedException
public final native java.lang.Class java.lang.Object.getClass()
public final native void java.lang.Object.notify()
public final native void java.lang.Object.notifyAll()
public default java.util.stream.IntStream java.lang.CharSequence.chars()
public default java.util.stream.IntStream java.lang.CharSequence.codePoints()
public java.lang.String(byte[],int,int)
public java.lang.String(byte[],java.nio.charset.Charset)
public java.lang.String(byte[],java.lang.String) throws java.io.UnsupportedEncodingException
public java.lang.String(byte[],int,int,java.nio.charset.Charset)
public java.lang.String(byte[],int,int,java.lang.String) throws java.io.UnsupportedEncodingException
public java.lang.String(java.lang.StringBuilder)
public java.lang.String(java.lang.StringBuffer)
public java.lang.String(byte[])
public java.lang.String(int[],int,int)
public java.lang.String()
public java.lang.String(char[])
public java.lang.String(java.lang.String)
public java.lang.String(char[],int,int)
public java.lang.String(byte[],int)
public java.lang.String(byte[],int,int,int)
Process finished with exit code 0
参数为java.lang.String,在idea中设置运行参数:Edit Configurations>Configuration>Program arguments,需要填写某个类的全限定名。
14.7 动态代理
代理是基本的设计模式之一,是用来代替实际对象的对象,通常涉及与实际对象的通信,充当中间人的角色。
package com.typeinfo;
/**
* @author [email protected]
* @since 2018-06-08 10:22
*/
public class SimpleProxyDemo {
public static void consumer(Interface inter) {
inter.doSomething();
inter.somethingElse("banana");
}
public static void main(String[] args) {
consumer(new RealObject());
consumer(new ProxyObject(new RealObject()));
}
}
interface Interface {
void doSomething();
void somethingElse(String arg);
}
class RealObject implements Interface {
@Override
public void doSomething() {
System.out.println("do something.");
}
@Override
public void somethingElse(String arg) {
System.out.println("do something else :" + arg);
}
}
class ProxyObject implements Interface {
private Interface proxied;
public ProxyObject(Interface proxied) {
this.proxied = proxied;
}
@Override
public void doSomething() {
System.out.println("proxy object do something.");
proxied.doSomething();
}
@Override
public void somethingElse(String arg) {
System.out.println("proxy object do something else: " + arg);
proxied.somethingElse(arg);
}
}
output:
do something.
do something else :banana
proxy object do something.
do something.
proxy object do something else: banana
do something else :banana
Process finished with exit code 0
当想要执行一些额外操作,同时想将这些操作从”实际”对象中分离出去时,或者想要更加容易的做出修改时代理是很有用的。
通过调用静态方法Proxy.newProxyInstance()可以创建动态代理对象,
newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)
Returns an instance of a proxy class for the specified interfaces that dispatches method invocations to the specified invocation handler.
package com.typeinfo;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
/**
* @author [email protected]
* @since 2018-06-08 10:35
*/
public class SimpleDynamicProxy {
public static void consumer(Interface inter) {
inter.doSomething();
inter.somethingElse("Banana");
}
public static void main(String[] args) {
RealObject realObject = new RealObject();
consumer(realObject);
Interface proxy = (Interface) Proxy.newProxyInstance(Interface.class.getClassLoader(),
new Class[]{Interface.class}, new DynamicProxyHandler(realObject));
consumer(proxy);
}
}
class DynamicProxyHandler implements InvocationHandler {
private Object proxied;
public DynamicProxyHandler(Object proxied) {
this.proxied = proxied;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("proxy: " + proxy.getClass() + " .method: " + method + " .args: " + args);
if (args != null) {
for (Object object : args) {
System.out.println(object + " ");
}
}
return method.invoke(proxied, args);
}
}
output:
do something.
do something else :Banana
proxy: class com.typeinfo.$Proxy0 .method: public abstract void com.typeinfo.Interface.doSomething() .args: null
do something.
proxy: class com.typeinfo.$Proxy0 .method: public abstract void com.typeinfo.Interface.somethingElse(java.lang.String) .args: [Ljava.lang.Object;@12a3a380
Banana
do something else :Banana
Process finished with exit code 0
14.8 空对象
当时用内置的null去表示空对象时,在每次使用这个对象的时候都需要先判断该对象是否为null,并且判断除了用来产生NullPointerException之外,没有任何帮助,还会产生大量冗余代码。我们希望有一个空对象,可以接收传传给它所代表的对象,但是将返回表示该对象不存在的任何真实对象的值。
最简单的是创建一个标记接口。
package com.typeinfo;
/**
* @author [email protected]
* @since 2018-06-08 11:02
*/
public class NullPerson extends Person implements Null {
private NullPerson() {
super("None", "None", "None");
}
public String toString() {
return "NullPerson";
}
public static final Person Null = new NullPerson();
}
interface Null {
}
class Person {
public final String first;
public final String last;
public final String address;
public Person(String first, String last, String address) {
this.first = first;
this.last = last;
this.address = address;
}
@Override
public String toString() {
return "Person{" +
"first='" + first + '\'' +
", last='" + last + '\'' +
", address='" + address + '\'' +
'}';
}
}
14.9 接口与类型信息
interface的一种重要目标就是允许隔离构件,进而降低耦合度,如果编写接口,那么可以实现这一目标,但是通过类型信息,这种耦合性还是会传播出去。
package com.typeinfo;
/**
* @author [email protected]
* @since 2018-06-08 11:13
*/
public class InterfaceViolation {
public static void main(String[] args) {
A a = new B();
a.f();
System.out.println(a.getClass().getCanonicalName());
if (a instanceof B) {
B b = (B) a;
b.g();
}
}
}
interface A {
void f();
}
class B implements A {
@Override
public void f() {
}
public void g() {
}
}
a被当作B实现,通过转型为B,可以调用A中不存在的方法。
一种解决方法是直接声明,使用实际的类而不是接口。最简单的方式是对实现使用包访问权限,这样在包外便无法访问。
package com.typeinfo;
/**
* @author [email protected]
* @since 2018-06-08 11:27
*/
public class HiddenC {
public static A makeA() {
return new C();
}
}
class C implements A {
@Override
public void f() {
}
public void g() {
}
void u() {
}
protected void v() {
}
private void w() {
}
}
即使从makeA()返回的是C类型,在包外依然无法使用A之外的任何方法。
package com.typeinfo;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
/**
* @author [email protected]
* @since 2018-06-08 11:30
*/
public class HiddenImplementation {
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException,
InvocationTargetException {
A a = HiddenC.makeA();
a.f();
System.out.println(a.getClass().getCanonicalName());
if (a instanceof C) {
C c = (C) a;
c.g();
}
callHiddenMethod(a, "g");
callHiddenMethod(a, "v");
callHiddenMethod(a, "u");
callHiddenMethod(a, "w");
}
static void callHiddenMethod(Object object, String methodName) throws NoSuchMethodException,
InvocationTargetException, IllegalAccessException {
Method g = object.getClass().getDeclaredMethod(methodName);
g.setAccessible(true);
g.invoke(object);
}
}
output:
public f().
com.typeinfo.C
public g().
public g().
protected v().
package u().
private w().
Process finished with exit code 0
通过反射,仍旧可以调用所有方法,甚至是private方法,如果知道方法名,可以在Method对象上调用setAccessible(true)。
甚至使用发布编译后的代码也无法阻止这种情况,因为在发布的jdk中有反编译器便可突破这一限制。
内部类与匿名类也无法阻止反射到达并调用非公共访问权限的方法。
package com.typeinfo;
import java.lang.reflect.InvocationTargetException;
/**
* @author [email protected]
* @since 2018-06-08 11:46
*/
public class InnerImplementation {
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException {
A a = InnerA.makeA();
a.f();
System.out.println(a.getClass().getName());
HiddenImplementation.callHiddenMethod(a, "g");
HiddenImplementation.callHiddenMethod(a, "u");
HiddenImplementation.callHiddenMethod(a, "v");
HiddenImplementation.callHiddenMethod(a, "w");
}
}
class InnerA {
private static class C implements A {
@Override
public void f() {
System.out.println("C.f()");
}
public void g() {
System.out.println("C.g()");
}
void u() {
System.out.println("C.u()");
}
protected void v() {
System.out.println("C.v()");
}
private void w() {
System.out.println("C.w()");
}
}
public static A makeA(){
return new C();
}
}
output:
C.f()
com.typeinfo.InnerA$C
C.g()
C.u()
C.v()
C.w()
Process finished with exit code 0
package com.typeinfo;
import java.lang.reflect.InvocationTargetException;
/**
* @author [email protected]
* @since 2018-06-08 11:51
*/
public class AnonymousImplementation {
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException,
InvocationTargetException {
A a = AnonymousA.makeA();
a.f();
System.out.println(a.getClass().getName());
HiddenImplementation.callHiddenMethod(a, "g");
HiddenImplementation.callHiddenMethod(a, "u");
HiddenImplementation.callHiddenMethod(a, "v");
HiddenImplementation.callHiddenMethod(a, "w");
}
}
class AnonymousA {
public static A makeA() {
return new 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 u()");
}
protected void v() {
System.out.println("protected v()");
}
private void w() {
System.out.println("private w()");
}
};
}
}
output:
public c.f()
com.typeinfo.AnonymousA$1
public c.g()
package u()
protected v()
private w()
Process finished with exit code 0