Java编程思想,Java学习必读经典,不管是初学者还是大牛都值得一读,这里总结书中的重点知识,这些知识不仅经常出现在各大知名公司的笔试面试过程中,而且在大型项目开发中也是常用的知识,既有简单的概念理解题(比如is-a关系和has-a关系的区别),也有深入的涉及RTTI和JVM底层反编译知识。
为什么会这样输出?这就要明确掌握Java中构造函数的调用顺序:
(1)在其他任何事物发生之前,将分配给对象的存储空间初始化成二进制0; (2)调用基类构造函数。从根开始递归下去,因为多态性此时调用子类覆盖后的draw()方法(要在调用RoundGlyph构造函数之前调用),由于步骤1的缘故,我们此时会发现radius的值为0; (3)按声明顺序调用成员的初始化方法; (4)最后调用子类的构造函数。
基类和子类有着完全相同的接口,这样向上转型时永远不需要知道正在处理的对象的确切类型,这通过多态来实现。
缺点就是子类中接口的扩展部分不能被基类访问,因此一旦向上转型,就不能调用那些新方法。
Shape s = (Shape)s1;
Class.forName()
。instanceof
,它返回一个bool值,它保持了类型的概念,它指的是“你是这个类吗?或者你是这个类的派生类吗?”。而如果用==或equals比较实际的Class对象,就没有考虑继承—它或者是这个确切的类型,或者不是。Class对象
的特殊对象完成的,它包含了与类有关的信息。Java送Class对象来执行其RTTI,使用类加载器的子系统实现。无论何时,只要你想在运行时使用类型信息,就必须首先获得对恰当的Class对象的引用,获取方式有三种:
(1)如果你没有持有该类型的对象,则Class.forName()
就是实现此功能的便捷途,因为它不需要对象信息;
(2)如果你已经拥有了一个感兴趣的类型的对象,那就可以通过调用getClass()
方法来获取Class引用了,它将返回表示该对象的实际类型的Class引用。Class包含很有有用的方法,比如:
package rtti;interface HasBatteries{}interface WaterProof{}interface Shoots{}class Toy {
Toy() {}
Toy(int i) {}}class FancyToy extends Toyimplements HasBatteries, WaterProof, Shoots {
FancyToy() {
super(1);
}}public class RTTITest {
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());
}
public static void main(String[] args) {
Class c = null;
try {
c = Class.forName("rtti.FancyToy"); // 必须是全限定名(包名+类名)
} catch(ClassNotFoundException e) {
System.out.println("Can't find FancyToy");
System.exit(1);
}
printInfo(c);
for(Class face : c.getInterfaces()) {
printInfo(face);
}
Class up = c.getSuperclass();
Object obj = null;
try {
// Requires default constructor.
obj = up.newInstance();
} catch (InstantiationException e) {
System.out.println("Can't Instantiate");
System.exit(1);
} catch (IllegalAccessException e) {
System.out.println("Can't access");
System.exit(1);
}
printInfo(obj.getClass());
}}
输出:
Class name: rtti.FancyToy, is interface? [false] Simple name: FancyToy Canonical name: rtti.FancyToy Class name: rtti.HasBatteries, is interface? [true] Simple name: HasBatteries Canonical name: rtti.HasBatteries Class name: rtti.WaterProof, is interface? [true] Simple name: WaterProof Canonical name: rtti.WaterProof Class name: rtti.Shoots, is interface? [true] Simple name: Shoots Canonical name: rtti.Shoots Class name: rtti.Toy, is interface? [false] Simple name: Toy Canonical name: rtti.Toy
(3)Java还提供了另一种方法来生成对Class对象的引用,即使用类字面常量。比如上面的就像这样:FancyToy.class;
来引用。
这样做不仅更简单,而且更安全,因为它在编译时就会受到检查(因此不需要置于try语句块中),并且它根除了对forName方法的引用,所以也更高效。类字面常量不仅可以应用于普通的类,也可以应用于接口、数组以及基本数据类型。
注意:当使用“.class”来创建对Class对象的引用时,不会自动地初始化该Class对象,初始化被延迟到了对静态方法(构造器隐式的是静态的)或者非final静态域(注意final静态域不会触发初始化操作)进行首次引用时才执行:。而使用Class.forName时会自动的初始化。
为了使用类而做的准备工作实际包含三个步骤: - 加载:由类加载器执行。查找字节码,并从这些字节码中创建一个Class对象 - 链接:验证类中的字节码,为静态域分配存储空间,并且如果必需的话,将解析这个类创建的对其他类的所有引用。 - 初始化:如果该类具有超类,则对其初始化,执行静态初始化器和静态初始化块。
这一点非常重要,下面通过一个实例来说明这两者的区别:
package rtti;import java.util.Random;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) {
// Does not trigger initialization
Class initable = Initable.class;
System.out.println("After creating Initable ref");
// Does not trigger initialization
System.out.println(Initable.staticFinal);
// Does trigger initialization(rand() is static method)
System.out.println(Initable.staticFinal2);
// Does trigger initialization(not final)
System.out.println(Initable2.staticNonFinal);
try {
Class initable3 = Class.forName("rtti.Initable3");
} catch (ClassNotFoundException e) {
System.out.println("Can't find Initable3");
System.exit(1);
}
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
可以突破这个限制吗?是的,突破它的就是反射机制。
Class
类与java.lang.reflect
类库一起对反射的概念进行了支持,该类库包含了Field
、Method
以及Constructor
类(每个类都实现了Member
接口)。这些类型的对象是由JVM在运行时创建的,用以表示未知类里对应的成员。这样你就可以使用Constructor
创建新的对象,用get()/set()
方法读取和修改与Field
对象关联的字段,用invoke()
方法调用与Method
对象关联的方法。另外,还可以调用getFields()、getMethods()和getConstructors()
等很便利的方法,以返回表示字段、方法以及构造器的对象的数组。这样,匿名对象的类信息就能在运行时被完全确定下来,而在编译时不需要知道任何事情。
####反射与RTTI的区别
当通过反射与一个未知类型的对象打交道时,JVM只是简单地检查这个对象,看它属于哪个特定的类(就像RTTI那样),在用它做其他事情之前必须先加载那个类的Class
对象,因此,那个类的.class
文件对于JVM来说必须是可获取的:要么在本地机器上,要么可以通过网络取得。所以RTTI与反射之间真正的区别只在于:对RTTI来说,编译器在编译时打开和检查.class文件(也就是可以用普通方法调用对象的所有方法);而对于反射机制来说,.class文件在编译时是不可获取的,所以是在运行时打开和检查.class文件。
下面的例子是用反射机制打印出一个类的所有方法(包括在基类中定义的方法):
package typeinfo;import java.lang.reflect.Constructor;import java.lang.reflect.Method;import java.util.regex.Pattern;// Using reflection to show all the methods of a class.// even if the methods are defined in the base class.public class ShowMethods {
private static String usage =
"usage: \n" +
"ShowMethods qualified.class.name\n" +
"To show all methods in class or: \n" +
"ShowMethods qualified.class.name word\n" +
"To search for methods involving 'word'";
private static Pattern p = Pattern.compile("\\w+\\.");
public static void main(String[] args) {
if(args.length < 1) {
System.out.println(usage);
System.exit(0);
}
int lines = 0;
try {
Class<?> c = Class.forName(args[0]);
Method[] methods = c.getMethods();
Constructor[] ctors = c.getConstructors();
if(args.length == 1) {
for(Method method : methods) {
System.out.println(p.matcher(method.toString()).replaceAll(""));
}
for(Constructor ctor : ctors) {
System.out.println(p.matcher(ctor.toString()).replaceAll(""));
}
lines = methods.length + ctors.length;
} else {
for(Method method : methods) {
if(method.toString().indexOf(args[1]) != -1) {
System.out.println(p.matcher(method.toString()).replaceAll(""));
lines++;
}
}
for(Constructor ctor : ctors) {
if(ctor.toString().indexOf(args[1]) != -1) {
System.out.println(p.matcher(ctor.toString()).replaceAll(""));
lines++;
}
}
}
} catch (ClassNotFoundException e) {
System.out.println("No such Class: " + e);
}
}}
输出:
public static void main(String[]) public final native void wait(long) throws InterruptedException public final void wait() throws InterruptedException public final void wait(long,int) throws InterruptedException public boolean equals(Object) public String toString() public native int hashCode() public final native Class getClass() public final native void notify() public final native void notifyAll() public ShowMethods()
Java虚拟机中有许多附加技术用以提升速度,尤其是与加载器操作相关的,被称为“即时”(Just-In-Time,JIT)编译器的技术。这种技术可以把程序全部或部分翻译成本地机器码(这本来是JVM的工作),程序运行速度因此得以提升。当需要装载某个类时,编译器会先找到其.class文件,然后将该类的字节码装入内存。此时,有两种方案可供选择: (1)一种就是让即时编译器编译所有代码。但这种做法有两个缺陷:这种加载动作散落在整个程序生命周期内,累加起来要花更多时间;并且会增加可执行代码的长度(字节码要比即时编译器展开后的本地机器码小很多),这将导致页面调度,从而降低程序速度。 (2)另一种做法称为惰性评估(lazy evaluation),意思是即时编译器只在必要的时候才编译代码,这样,从不会被执行的代码也许就压根不会被JIT所编译。新版JDK中的Java HotSpot技术就采用了类似方法,代码每次被执行的时候都会做一些优化,所以执行的次数越多,它的速度就越快。
在最近的Java版本中,虚拟机(特别是hotspot技术)能自动侦测这些情况,并颇为“明智”地决定是否嵌入一个final 方法。然而,最好还是不要完全相信编译器能正确地作出所有判断。通常,只有在方法的代码量非常少,或者想明确禁止方法被覆盖的时候,才应考虑将一个方法设为final。
类内所有private 方法都自动成为final。由于我们不能访问一个private 方法,所以它绝对不会被其他方法覆盖(若强行这样做,编译器会给出错误提示)。可为一个private方法添加final指示符,但却不能为那个方法提供任何额外的含义。
使用内部类,还可以获得其他一些特性: - 内部类可以有多个实例,每个实例都有自己的状态信息,并且与其外围类对象的信息相互独立。 - 在单个外围类中,可以让多个内部类以不同的方式实现同一个接口或继承同一个类。 - 创建内部类对象的时刻并不依赖于外围类对象的创建。 - 内部类并没有令人迷惑的is-a关系,它就是一个独立的实体。
StringBuilder
操作(javap -c class字节码文件名 命令
查看具体优化过程)。这让你觉得可以随意使用String对象,反正编译器会为你自动地优化性能。但编译器能优化到什么程度还不好说,不一定能优化到使用StringBuilder代替String相同的效果。比如:
public class WitherStringBuilder { 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(); }}
运行javap -c WitherStringBuilder
,可以看到两个方法对应的字节码。
implicit方法:
public java.lang.String implicit(java.lang.String[]);
Code:
0: ldc #16 // String
2: astore_2
3: iconst_0
4: istore_3
5: goto 32
8: new #18 // class java/lang/StringBuilder
11: dup
12: aload_2
13: invokestatic #20 // Method java/lang/String.valueOf:(
Ljava/lang/Object;)Ljava/lang/String;
16: invokespecial #26 // Method java/lang/StringBuilder.”<
init>”:(Ljava/lang/String;)V
19: aload_1
20: iload_3
21: aaload
22: invokevirtual #29 // Method java/lang/StringBuilder.ap
pend:(Ljava/lang/String;)Ljava/lang/StringBuilder;
25: invokevirtual #33 // Method java/lang/StringBuilder.to
String:()Ljava/lang/String;
28: astore_2
29: iinc 3, 1
32: iload_3
33: aload_1
34: arraylength
35: if_icmplt 8
38: aload_2
39: areturn
public java.lang.String implicit(java.lang.String[]);
Code:
0: ldc #16 // String
2: astore_2
3: iconst_0
4: istore_3
5: goto 32
8: new #18 // class java/lang/StringBuilder
11: dup
12: aload_2
13: invokestatic #20 // Method java/lang/String.valueOf:(
Ljava/lang/Object;)Ljava/lang/String;
16: invokespecial #26 // Method java/lang/StringBuilder.”<
init>”:(Ljava/lang/String;)V
19: aload_1
20: iload_3
21: aaload
22: invokevirtual #29 // Method java/lang/StringBuilder.ap
pend:(Ljava/lang/String;)Ljava/lang/StringBuilder;
25: invokevirtual #33 // Method java/lang/StringBuilder.to
String:()Ljava/lang/String;
28: astore_2
29: iinc 3, 1
32: iload_3
33: aload_1
34: arraylength
35: if_icmplt 8
38: aload_2
39: areturn可以发现,StringBuilder是在循环之内构造的,这意味着每经过循环一次,就会创建一个新的StringBuilder对象。
explicit方法:
public java.lang.String explicit(java.lang.String[]); Code: 0: new #18 // class java/lang/StringBuilder 3: dup 4: invokespecial #45 // Method java/lang/StringBuilder.”< init>”:()V 7: astore_2 8: iconst_0 9: istore_3 10: goto 24 13: aload_2 14: aload_1 15: iload_3 16: aaload 17: invokevirtual #29 // Method java/lang/StringBuilder.ap pend:(Ljava/lang/String;)Ljava/lang/StringBuilder; 20: pop 21: iinc 3, 1 24: iload_3 25: aload_1 26: arraylength 27: if_icmplt 13 30: aload_2 31: invokevirtual #33 // Method java/lang/StringBuilder.to String:()Ljava/lang/String; 34: areturn }
可以看到,不仅循环部分的代码更简短、更简单,而且它只生成了一个StringBuilder对象。显式的创建StringBuilder还允许你预先为其指定大小。如果你已经知道最终的字符串大概有多长,那预先指定StringBuilder的大小可以避免多次重新分配缓冲。
####总结 因此,当你为一个类重写toString()方法时,如果字符串操作比较简单,那就可以信赖编译器,它会为你合理地构造最终的字符串结果。但是,如果你要在toString()方法中使用循环,那么最好自己创建一个StringBuilder对象,用它来构造最终的结果。
System.out.printf()
和System.out.format()
方法模仿自C的printf
,可以格式化字符串,两者是完全等价的。Java中,所有新的格式化功能都由java.util.Formatter
类处理。
String.format()
方法参考了C中的sprintf()
方法,以生成格式化的String对象,是一个static方法,它接受与Formatter.format()
方法一样的参数,但返回一个String对象。当你只需使用format()
方法一次的时候,该方法很方便。
import java.util.Arrays;import java.util.Formatter;public class SimpleFormat {
public static void main(String[] args) {
int x = 5;
double y = 5.324667;
System.out.printf("Row 1: [%d %f]\n", x, y);
System.out.format("Row 1: [%d %f]\n", x, y);
Formatter f = new Formatter(System.out);
f.format("Row 1: [%d %f]\n", x, y);
String str = String.format("Row 1: [%d %f]\n", x, y);
System.out.println(str);
Integer[][] a = {
{1, 2, 3}, {4, 5, 6},
{7, 8, 3}, {9, 10, 6}
};
System.out.println(Arrays.deepToString(a));
}}
java.io.InvalidException: no valid constructor
异常,因此Externalizable对象必须有默认构造函数,而且必需是public的。
- Externalizable
的替代方法:如果不是特别坚持实现Externalizable接口,那么还有另一种方法。我们可以实现Serializable
接口,并添加writeObject()
和readObject()
的方法。一旦对象被序列化或者重新装配,就会分别调用那两个方法。也就是说,只要提供了这两个方法,就会优先使用它们,而不考虑默认的序列化机制。
这些方法必须含有下列准确的签名:
private void writeObject(ObjectOutputStream stream) throws IOException;private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException
- 可以用transient
关键字逐个字段地关闭序列化,它的意思是“不用麻烦你保存或恢复数据—我自己会处理的”。由于Externalizable对象在默认情况下不保存它们的任何字段,所以transient关键字只能和Serializable对象一起使用。Externalizable
代替实现Serializable
接口来对序列化过程进行控制,Externalizable
继承了Serializable
接口,同时增添了两个方法:writeExternal()
和readExternal()
。