13/14:字符串与类型信息

字符串

String—不可变的对象

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

public class Immutable {
  public static String upcase(String s) {
    return s.toUpperCase();
  }
  public static void main(String[] args) {
    String q = "howdy";
    print(q); // howdy
    String qq = upcase(q);
    print(qq); // HOWDY
    print(q); // howdy
  }
}

如上面的程序中,String的toUpperCase方法会重新创建一个String对象并返回。

在这里提一点,final修饰的类传给形参,实际传的也是引用。即地址没有改变。

重载“+” 与 StringBuilde

看了上面的内容,String是不可变的对象,你可能会觉得下面的代码会应该是由几个String对象组合而成的。然而事实并非如此。编译器自动引入了StringBuilder类,通过append方法生成结果,并存为s。

String s = "abc" + "mango" + 47 + "aaa";

但是如果是字符串中使用循环,编译器可能会生成多个StringBuilder对象。这时候最好使用StringBuilder而不是String对象。如下:

public class WhitherStringBuilder {
    public String implicit(String[] fields) {   //不推荐使用
        String result = "";
        for (int i = 0; i < fields.length; i++)
            result += fields[i];
        return result;
    }

    public String explicit(String[] fields) {   //推荐使用
        StringBuilder result = new StringBuilder();
        for (int i = 0; i < fields.length; i++)
            result.append(fields[i]);
        return result.toString();
    }
}

StringBuilder提供了丰富的方法,包括insert()repleace()substring()reverse()

查看编译后的StringBuilder创建了几个StringBuilder的方法:https://blog.csdn.net/ricky_yangrui/article/details/81941299

无意识的递归

举一个例子,如ArrayList.toString(),它会遍历ArrayList中包含的所有对象,调用每个元素上的toString()方法。

如果希望toString打印的是内存地址,需要使用super.toString(),也就是Object.toString()

格式化输出

System.out.format()

java可以使用System.out.format("我是%s", "一颗遗失的星星");来进行格式化输出。

有一些特殊符号表示:%d:用作int类型输出、%f:用作浮点型输出,如:

System.out.format("Row 1: [%d %f]\n", x, y);
// or
System.out.printf("Row 1: [%d %f]\n", x, y);

Formatter与格式化说明符

如果需要输出表格,可以用下面的形式:

public class Receipt {
    private double total = 0;
    private Formatter f = new Formatter(System.out);

    public void printTitle() {
        f.format("%-30s %5s %10s\n", "Item", "Qty", "Price");
        f.format("%-30s %5s %10s\n", "----", "---", "-----");
    }

    public void print(String name, int qty, double price) {
        f.format("%-30.15s %5d %10.2f\n", name, qty, price);
        total += price;
    }

    public void printTotal() {
        f.format("%-30s %5s %10.2f\n", "Tax", "", total * 0.06);
        f.format("%-30s %5s %10s\n", "", "", "-----");
        f.format("%-30s %5s %10.2f\n", "Total", "",
                total * 1.06);
    }

    public static void main(String[] args) {
        Receipt receipt = new Receipt();
        receipt.printTitle();
        receipt.print("Jack's Magic Beans Jack's Magic Beans Jack's Magic Beans", 4, 4.25);
        receipt.print("Princess Peas", 3, 5.1);
        receipt.print("Three Bears Porridge", 1, 14.29);
        receipt.printTotal();
    }
} /* Output:
Item              Qty      Price
----              ---      -----
Jack's Magic Be     4       4.25
Princess Peas       3       5.10
Three Bears Por     1      14.29
Tax                         1.42
                           -----
Total                      25.06
*///:~

String.format()

其实String.format()内部,也是创建了一个Formatter对象,上面的代码中可以直接替换成String.format()

正则表达式

正则表达式可以解决各种字符串处理相关的问题:匹配、选择、编辑、验证。

\\ 表示一个正则表达式的反斜线,后面的字符具有特殊意义。例如表示一位数字:\\d。如果想插入普通反斜线:\\\\

但是换行使用单反斜线:\n\t

若想表示“可能有一个负号,后面跟着一位或多位数字”,可以这样表示:-?\\d+

public class IntegerMatch {
    public static void main(String[] args) {
        System.out.println("-1234".matches("-?\\d+"));
        System.out.println("5678".matches("-?\\d+"));
        System.out.println("+911".matches("-?\\d+"));
        System.out.println("+911".matches("(-|\\+)?\\d+"));     //4
    }
}

()表示分组;|表示或操作;?表示出现一次或没有出现;

上面4处,(-|\\+)?表示字符串的起始字符可能是一个-++前要加\\转为普通字符),或二者皆没有(?修饰)

Spring中的Split()

Spring的Split()表示“将字符串从正则表达式匹配的地方切开”

public class Splitting {
    public static String knights =
            "Then, when you have found the shrubbery, you must " +
                    "cut down the mightiest tree in the forest... " +
                    "with... a herring!";

    public static void split(String regex) {
        System.out.println(
                Arrays.toString(knights.split(regex)));
    }

    public static void main(String[] args) {
        split(" "); // Doesn't have to contain regex chars
        split("\\W+"); // Non-word characters
        split("n\\W+"); // 'n' followed by non-word characters
    }
}

创建正则表达式

B 指定字符B
\xhh 十六进制为oxhh
\uhhhh 十六进制表示为oxhhhh的Unicode字符
\t 制表符Tab
\n 换行符
\r 回车
\f 换页
\e 转义

一些常用表达式

. 任意字符
[abe] 包含a、b和c的任何字符(和able作用相同)
[a-zA-Z] 从a到z或从A到Z的任意字符(范围)
[abc[hij]] 任意a、b、c、h、i和j字符(与a|b|c|h|i|j作用相同)(合并)
[a-z&&[hij]] 任意h、i或j(交)
\s 空白符(空格、tab、换行、换页和回车)
\S 非空白符([^\s])
\d 数字[0-9]
\D 非数字[^0-9]
\w 词字符[a-zA-Z0-9]
\W 非词字符[^\w]

逻辑操作符

XY Y跟在X后
X|Y X或Y
(X) 捕获组。

边界匹配符

^ 一行的起始 \B 非词的边界
$ 一行的结束 \G 前一个匹配的结束
\b 词的边界

我们学习正则表达式,够用就行了,所以这里不写太多,若想了解的朋友可以翻阅相关资料,其中,下面的网站或许对你生成java的正则表达式程序有帮助。

https://www.sojson.com/regex/generate

类型信息

java是如何让我们在运行时识别对象和类的信息?主要有两种方式:一种是传统的RTTI(运行时类型识别),它假定我们在编译时已经知道了所有的类型;另一种是“反射”机制。

为什么需要RTTI(运行时类型识别)

如果你希望大部分代码尽可能少地了解对象的具体类型,而只是与对象家族的某一个通用表示打交道,通俗地说只使用对象家族中的某个行为(方法),比如让一个动物家族“叫”,或“跳”,但是不关心是什么动物。使用多态会更容易写,更容易读,且更便于维护;设计业更容易实现、理解和改变。所以,动态是面向对象编程的基本目标。

如果我们想要对某个对象家族进行批量操作,比如,对所有形状的对象(三角形、四边形等)进行旋转,但是忽略圆(对圆旋转没有意义),我们就可以使用RTTI,查询出对象确切类型,从而剔除特例。

Class对象

要理解RTTI(运行时类型识别,下面简称RTTI)在java中的工作原理,首先要理解Class对象。Class对象就是用来创建类的所有的“常规”对象的。java使用Class对象来执行其RTTI。

每个类都有一个Class对象。为了生成这个类的对象,运行这个程序的java虚拟机(JVM)将使用被称为“类加载器”的子系统。

类加载器子系统实际上可以包含一条类加载器链,但只有一个原生类加载器,它是jvm的一部分。

所有的类都是在对其第一次使用时,动态加载到JVM中的。当程序创建第一个对类的静态成员的引用时,就会加载这个类。这个证明构造器也是类的静态方法

因此,java程序在它开始运行之前并非被完全加载,其各个部分是在必须时才被加载的。

类加载器首先检查这个类的Class对象是否已经被加载。如果未加载,默认的类加载器就会根据类名查找.class文件。在这个类被加载时,它们会接收验证,以确保其没有被破坏,并且不包含不良java代码。

一旦某个类的Class对象被载入内存,它就被用来创建这个类的所有对象。下面的例子可以说明这一点

class Candy {
    static {
        print("Loading Candy");
    }
}
class Gum {
    static {
        print("Loading Gum");
    }
}
class Cookie {
    static {
        print("Loading Cookie");
    }
}
public class SweetShop {
    public static void main(String[] args) {
        print("inside main");
        new Candy();
        print("After creating Candy");
        try {
            Class.forName("typeinfo.Gum");
        } catch (ClassNotFoundException e) {
            print("Couldn't find Gum");
        }
        print("After Class.forName(\"Gum\")");
        new Cookie();
        print("After creating Cookie");
    }
}

无论何时,只要你想在运行时使用类型信息,就必须首先获得对恰当的Class对象的引用。Class.forName()是实现途径,因为你不需要为了获得Class引用而持有该类型的对象。若已有类型的对象,可以通过getClass()来获取Class引用。Class包含很多有用的方法,下面是其中一部分。

Class.forName(pakage.ClassName)返回一个class对象,必须使用全限定名;

getName()产生全限定的类名,getSimpleName()产生不包含包名的类名和getCannonicalName()全限定的类名。

getInterfaces()返回Class对象,表示在感兴趣的Class对象中所包含的接口。

getSuperclass()查询直接基类

newInstance()是实现“虚拟构造器”的一种途径,它表示:“我不知道你的确切类型,但是无论如何要正确创建你自己。”,使用newInstance()来创建的类,必须带有默认构造器。

类字面常量— Object.class

java还提供了另一种方法生成Class对象的引用:类字面常量

FancyToy.class;

这样做对比Class.forName()来说:更简单、更安全(编译时就会收到检查,不用置于try块中)、更高效(根除了forName()方法的调用)。

类字面敞亮不仅可以应用与普通类,也可以应用于接口、数组、基本数据类型。对于基本类型的包装器类,有一个标准字段TYPE,指向对应的基本数据类型的Class对象。

TYPE和Class_2019-07-08_16-40-21

建议使用.class的形式,与普通类保持一致。

注意:当使用.class来创建对Class对象的引用时,不会自动地初始化该Class对象。为了使用类需要3个步骤:

  1. 加载,由类加载器执行。
  2. 链接。在链接阶段将验证类中的字节码,为静态域分配存储空间,并且如果必须的话,将解析这个类创建的对其他类的所有引用。
  3. 初始化。如果该类具有超类,则对其初始化,执行静态初始化器和静态初始化块。
class Initable {
    static final int staticFinal = 47;
    static final int staticFinal2 = 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.staticFinal);
        // Does trigger initialization:
        System.out.println(Initable.staticFinal2);
        // 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);
    }
}/* Output:
After creating Initable ref
47
Initializing Initable
258
Initializing Initable2
147
Initializing Initable3
After creating Initable3 ref
74
*///:~

如上面程序,仅仅使用.class获取类的引用不会发生初始化,而Class.forName()会立即引发初始化。

如果一个static final 值是“编译期常量”,就像Initable.staticFinal 那样,那么这个值不需要对Initable类进行初始化就可以被读取。但是像Initable.staticFinal2的访问将枪支进行类初始化(它不是一个“编译期常量”)

泛化的Class引用

Class引用总是指向某个Class对象,它可以制造类的实例,并包含可作用于这些实例的所有方法代码。

通过使用泛型语法,可以让编译器枪支执行额外的类型检查:

public class GenericClassReferences {
    public static void main(String[] args) {
        Class intClass = int.class;
        Class<Integer> genericIntClass = int.class;
        genericIntClass = Integer.class; // Same thing
        intClass = double.class;    //1
        //genericIntClass = double.class; // Illegal
    }
} ///:~

代码1处对普通Class没有警告信息,但是下行使用泛型则编译出错。

Class<Number> genericNumberClass = int.class;

上面代码似乎不出错(Integer继承自Number)。但是它无法工作(Integer Class对象不是Number Class对象的子类)。这点在后面的章节讨论。

Class<?>

在泛型中使用?表示“任何事物”,下面的程序可以编译通过:

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

在java se5中,Class<?>优于平凡的Class,即便它们等价。Class<?>的好处是它表示你并非由于疏忽,而是你选择了非具体的版本。

Class<?>与extends结合

Class<?>与extends结合,可以创建一个范围,或者说限定为某种类型,或者该类型的任何子类型:

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对象时,newInstance()会返回确切的类型。这在某种程度上有些受限:

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<? super FancyToy> up只是说是FancyToy的超类,而不能直接接受 Class<Toy>up.newInstance()的返回不是很精确,只能是Object类型。

新的转义语法case()

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);    //1
        h = (House) b; // ... or just do this.  2
    }
} ///:~

1很少用,2少了很多工作。

类型转换前先做检查

我们已知的RTTI(运行时类型识别)形式包括:

  1. 传统的类型转换,如向下转型。由RTTI(运行时类型识别)。错误时抛出ClassCastException异常。

  2. 代表对象的类型的Class对象。通过查询Class对象可以获取运行时所需的信息。

  3. RTTI在java中海油第三种形式,就是关键字instanceof

    if(x instanceof Dog)
     (Dog)x;

动态的instanceof

Class.isInstance方法提供了一种动态的测试对象的途径。

public class PetCount3 {
    static class PetCounter
            extends LinkedHashMap<Class<? extends Pet>, Integer> {
        public PetCounter() {
            super(MapData.map(LiteralPetCreator.allTypes, 0));
        }

        public void count(Pet pet) {
            // Class.isInstance() eliminates instanceofs:
            for (Map.Entry<Class<? extends Pet>, Integer> pair
                    : entrySet())
                //使用isInstance()动态验证对象类型
                if (pair.getKey().isInstance(pet))
                    put(pair.getKey(), pair.getValue() + 1);
        }

        public String toString() {
            StringBuilder result = new StringBuilder("{");
            for (Map.Entry<Class<? extends Pet>, Integer> pair
                    : entrySet()) {
                result.append(pair.getKey().getSimpleName());
                result.append("=");
                result.append(pair.getValue());
                result.append(", ");
            }
            result.delete(result.length() - 2, result.length());
            result.append("}");
            return result.toString();
        }
    }

    public static void main(String[] args) {
        PetCounter petCount = new PetCounter();
        for (Pet pet : Pets.createArray(20)) {
            printnb(pet.getClass().getSimpleName() + " ");
            petCount.count(pet);
        }
        print();
        print(petCount);
    }
} /* Output:
Rat Manx Cymric Mutt Pug Cymric Pug Manx Cymric Rat EgyptianMau Hamster EgyptianMau Mutt Mutt Cymric Mouse Pug Mouse Cymric
{Pet=20, Dog=6, Cat=9, Rodent=5, Mutt=3, Pug=3, EgyptianMau=2, Manx=7, Cymric=5, Rat=2, Mouse=2, Hamster=1}
*///:~

可以看到上面代码的好处是添加新类型时,只需要改变LiteralPetCreator.java的数组即可(这里所有的代码都可以在《java编程思想第四版》这本书中找到),而不需要改变上面的代码。

递归计数

isAssignableFrom()校验传递的对象确实属于一个特定的继承结构,比如:Mouse.class.isAssignableFrom(Pet.class)

下面是使用递归的方式计数:

public class TypeCounter extends HashMap<Class<?>, Integer> {
    private Class<?> baseType;

    public TypeCounter(Class<?> baseType) {
        this.baseType = baseType;
    }

    public void count(Object obj) {
        Class<?> type = obj.getClass();
        if (!baseType.isAssignableFrom(type))   //判断类型是否属于某种继承结构
            throw new RuntimeException(obj + " incorrect type: "
                    + type + ", should be type or subtype of "
                    + baseType);
        countClass(type);
    }

    private void countClass(Class<?> type) {
        Integer quantity = get(type);
        put(type, quantity == null ? 1 : quantity + 1);
        Class<?> superClass = type.getSuperclass();
        if (superClass != null &&
                baseType.isAssignableFrom(superClass))
            countClass(superClass);     //递归计算父类数量
    }

    public String toString() {
        StringBuilder result = new StringBuilder("{");
        for (Map.Entry<Class<?>, Integer> pair : entrySet()) {
            result.append(pair.getKey().getSimpleName());
            result.append("=");
            result.append(pair.getValue());
            result.append(", ");
        }
        result.delete(result.length() - 2, result.length());
        result.append("}");
        return result.toString();
    }
  
    public static void main(String[] args) {
        TypeCounter counter = new TypeCounter(Pet.class);
        for(Pet pet : Pets.createArray(20)) {
            printnb(pet.getClass().getSimpleName() + " ");
            counter.count(pet);
        }
        print();
        print(counter);
    }
} 

instanceof与Class的等价性

以instanceof(或isInstance()的形式,它们产生的结果相同)的形式与直接比较Class对象有一个很重要的差别

  • instanceofisInstance()生成的结果完全一样,保持了类型的概念,指“你是这个类吗?或者你是这个类的派生类吗”
  • equals()==也一样,但是它们没有考虑继承,指是否是确切的类型。
class Base {
}
class Derived extends Base {
}
public class FamilyVsExactType {
    static void test(Object x) {
        print("Testing x of type " + x.getClass());
        print("x instanceof Base " + (x instanceof Base));
        print("x instanceof Derived " + (x instanceof Derived));
        print("Base.isInstance(x) " + Base.class.isInstance(x));
        print("Derived.isInstance(x) " +
                Derived.class.isInstance(x));
        print("x.getClass() == Base.class " +
                (x.getClass() == Base.class));
        print("x.getClass() == Derived.class " +
                (x.getClass() == Derived.class));
        print("x.getClass().equals(Base.class)) " +
                (x.getClass().equals(Base.class)));
        print("x.getClass().equals(Derived.class)) " +
                (x.getClass().equals(Derived.class)));
    }
    public static void main(String[] args) {
        test(new Derived());
    }
}

反射:运行时的类信息

人们想要在运行时获取类的信息的另一个动机,是希望提供在跨网络的远程平台上创建和运行对象的能力。这被称为远程方法调用(RMI)。它允许一个java程序将对象分布到多台机器上。为什么需要这样的能力?想想负载均衡就知道了。

Class类与java.lang.reflect类库一起对反射的概念进行了支持,该类库包含了Field、Method以及Constructor类(每个类都实现了Member接口)。这些类型的对象是有JVM在运行时创建的,用来表示未知类里对应的成员。这样就可以使用:

  • Constructor创建新的对象
  • 用get()和set()方法读取和修改与Field对象关联的字段
  • 用invoke()方法调用与Method对象关联的方法。
  • 还可以调用getFields()、getMethods()和getConstructors()返回字段、方法以及构造器的对象数组

这样,匿名对象的类信息就能在运行时被完全确定下来,而在编译时不需要知道任何事情。

反射机制并没有什么神奇之处。JVM只是简单地检查这个对象,看它属于哪个特定的类(就像RTTI那样)。在用它做其他事情之前要先加载那个类的Class对象:要么在本地机器上获取,要么在网络上获取。

反射与RTTI的区别是,RTTI在编译时打开和检查.class文件,而反射是在运行时打开和检查.class文件的。

类方法提取器

通常不需要直接用到反射,然而在你需要创建更加动态的代码时会很有用。反射在java中用来支持其他特性:对象序列化和JavaBean。

Class<?> c = Class.forName(args[0]);    //args[0]是接收到的参数
Method[] methods = c.getMethods();      //获得所有方法
Constructor[] ctors = c.getConstructors();      //获得所有构造函数

动态代理

代理是基本的设计模式之一,它是你为了提供额外的或不同的操作,而插入的用来代替“实际对象”的对象。这些操作通常涉及与“实际”对象的通信,因此代理通常充当“中间人”的角色。

下面是一个简单的代理模式的例子

interface Interface {
    void doSomething();

    void somethingElse(String arg);
}

class RealObject implements Interface {
    public void doSomething() {
        print("doSomething");
    }

    public void somethingElse(String arg) {
        print("somethingElse " + arg);
    }
}

class SimpleProxy implements Interface {
    private Interface proxied;

    public SimpleProxy(Interface proxied) {
        this.proxied = proxied;
    }

    public void doSomething() {
        print("SimpleProxy doSomething");
        proxied.doSomething();
    }

    public void somethingElse(String arg) {
        print("SimpleProxy somethingElse " + arg);
        proxied.somethingElse(arg);
    }
}

class SimpleProxyDemo {
    public static void consumer(Interface iface) {
        iface.doSomething();
        iface.somethingElse("bonobo");
    }

    public static void main(String[] args) {
        consumer(new RealObject());
        consumer(new SimpleProxy(new RealObject()));
    }
} /* Output:
doSomething
somethingElse bonobo
SimpleProxy doSomething
doSomething
SimpleProxy somethingElse bonobo
somethingElse bonobo
*///:~

在上诉代码中,SimpleProxy 增强了RealObject 的行为。

在任何时刻,只要你想要将额外的操作冲“实际”对象中分离到不同的地方,特别是当你希望很容易得做出修改,代理就很有用。

java的动态代理比代理的思想更向前迈进了一步。

class DynamicProxyHandler implements InvocationHandler {
    private Object proxied;

    public DynamicProxyHandler(Object proxied) {
        this.proxied = proxied;
    }

    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 arg : args)
                System.out.println("  " + arg);
        return method.invoke(proxied, args);    //1
    }
}

class SimpleDynamicProxy {
    public static void consumer(Interface iface) {
        iface.doSomething();
        iface.somethingElse("bonobo");
    }

    public static void main(String[] args) {
        RealObject real = new RealObject();
        consumer(real);
        // Insert a proxy and call again:
        Interface proxy = (Interface) Proxy.newProxyInstance(
                Interface.class.getClassLoader(),
                new Class[]{Interface.class},
                new DynamicProxyHandler(real));
        consumer(proxy);
    }
} 

通过调用静态方法Proxy.newProxyInstance()可以创建动态代理

(Interface) Proxy.newProxyInstance(
                Interface.class.getClassLoader(),
                new Class[]{Interface.class},
                new DynamicProxyHandler(real));
  • 第一个参数是一个类加载器。
  • 第二个参数是希望该代理实现的接口列表(不是类或抽象类)
  • 第三个参数是“实际”对象的引用,从而使得调用处理器在执行其中介任务时,可以将请求转发。

invoke()方法传递进来一个代理对象。在invoke()内部,在代理上调用方法时要格外小心,因为对接口的调用将被重定向为对代理的调用。

通常,会执行被代理的操作,然后使用Method.invoke()将请求转发给被代理对象,并传入必要参数,如上面彩色代码1处。

空对象

空对象可以接受传递给它的所代表的对象的消息,但是将返回表示为实际上并不存在的任何“真实”对象的值。

何时使用空对象?比如:许多系统都有一个Person类,而在代码中,有很多情况是你没有一个实际的人(或者你有,但是你还没有这个人的全部信息),这时,我们可以使用空对象。

以Person为例,创建一个空对象:

public interface Null {} 
class Person {
    public final String first;
    public final String last;
    public final String address;

    // etc.
    public Person(String first, String last, String address) {
        this.first = first;
        this.last = last;
        this.address = address;
    }

    public String toString() {
        return "Person: " + first + " " + last + " " + address;
    }

    public static class NullPerson
            extends Person implements Null {
        private NullPerson() {
            super("None", "None", "None");
        }

        public String toString() {
            return "NullPerson";
        }
    }

    public static final Person NULL = new NullPerson();
} ///:~

通常,空对象都是单例(用了final修饰)。只能在构造器中设置NullPerson的值,不能修改NullPerson。

如何测试一个空对象:使用instanceof探测泛华的Null还是更具体的NullPerson,由于是单例(地址不变,值不变),还可以使用equals()甚至==来与Person.Null比较。

猜你喜欢

转载自www.cnblogs.com/sean-zeng/p/11246295.html