字符串
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]
", x, y);
// or
System.out.printf("Row 1: [%d %f]
", 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
", "Item", "Qty", "Price");
f.format("%-30s %5s %10s
", "----", "---", "-----");
}
public void print(String name, int qty, double price) {
f.format("%-30.15s %5d %10.2f
", name, qty, price);
total += price;
}
public void printTotal() {
f.format("%-30s %5s %10.2f
", "Tax", "", total * 0.06);
f.format("%-30s %5s %10s
", "", "", "-----");
f.format("%-30s %5s %10.2f
", "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
。如果想插入普通反斜线:\\
。
但是换行使用单反斜线:
。
若想表示“可能有一个负号,后面跟着一位或多位数字”,可以这样表示:-?\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字符 |
制表符Tab | |
换行符 | |
回车 | |
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 | 前一个匹配的结束 |
词的边界 |
我们学习正则表达式,够用就行了,所以这里不写太多,若想了解的朋友可以翻阅相关资料,其中,下面的网站或许对你生成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对象。
建议使用.class
的形式,与普通类保持一致。
注意:当使用.class
来创建对Class对象的引用时,不会自动地初始化该Class对象。为了使用类需要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(运行时类型识别)形式包括:
-
传统的类型转换,如向下转型。由RTTI(运行时类型识别)。错误时抛出ClassCastException异常。
-
代表对象的类型的Class对象。通过查询Class对象可以获取运行时所需的信息。
-
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对象有一个很重要的差别
instanceof
和isInstance()
生成的结果完全一样,保持了类型的概念,指“你是这个类吗?或者你是这个类的派生类吗”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比较。