Java学习笔记整理
本文档是我个人整理的,首先是想通过完成本文档更加扎实自己的基础加强对java语言的理解,然后就是想给入了门的同志们做下贡献。
当然,本文档主要是对java语言基础(当然还有很多基础没有涉及到)的进行较全面的理解已经整理,只要充分掌握了基础知识,学习高级部分的知识才会事半功倍犹如轻车熟路一般容易上手。
正文:
CLASSPATH的设置:我们知道运行java必须要设置CLASSPATH环境变量,但是sun java 1.4之后改进了设计,JRE会自动搜索当前路径下的类文件,而且使用java的编译和运行工具时,系统会自动加载dt.jar和tools.jar文件中的类,因此不再需要设置该环境变量。
当然,如果你设置了CLASSPATH环境变量就必须设置全,比如包含当前路径,否则将会出现找不到类的问题。
Java程序的源文件必须与public类的类(接口)名相同,如果源文件所有类都没有使用public修饰,则文件名可以是任意合法的文件名,因此,一个Java源文件里最多自能定义一个public类(接口)。
运算符的结合性和优先级
只有 单目运算符,赋值运算符和三目运算符是从右向左结合的,其余全是左结合的。
运算符优先级
运算符说明 java运算符
分隔符 . [] () {} , ;
单目运算符 ++ -- ~ !
强制类型转换运算符 (type)
四则运算符和求余运算
移位运算符 << >> >>>
关系运算符 < <= > >= instanceof
等价运算符 == !=
按位与 &
按位异或 ^
按位或 |
条件与 &&
条件或 ||
三目运算符 ?:
赋值运算符 = += -= /= *= &= |= ^\ <<= >>= >>>= %=
break continue 后跟标签,可以直接结束其外层循环。 return 用于结束整个方法。
数组
定义数组时不能指定数组的长度。
foreach
当使用foreach 来迭代访问数组元素时,foreach中的循环变量相当于一个临时变量,整个临时变量并不是数组元素,它只是保存了数组元素的值(深复制),因此如果希望改变数组元素的值,则不能使用这种foreach循环。
栈内存和堆内存之分
当一个方法执行是,每个方法都会建立自己的内存栈,在这个方法内定义的变量将会逐个放入这块栈内存里,随着方法的执行结束,这个方法的内存栈也将被销毁,因此,所有在方法定义的变量都是放在栈内存中的。
当我们在程序中创建一个对象时,这个对象将被保存到运行时数据区中,这个运行时数据区就是堆内存。只有当一个对象没有任何引用变量引用它时,才会在合适的时机回收它。
面向对象
this可以代表任何对象,当this出现在某个方法体中时,它所代表的对象是不确定的,但它的类型是确定的,它所代表对象只能是当前类;只有当这个方法被调用时,他所代表的对象才能被确定下来。
static 修饰的方法中不能使用this引用,所以static修饰的方法中不能访问没有使用static修饰的普通成员。static 用来修饰方法和属性等成员。局部成员的上已经程序单元是方法不是类,使用static修饰它们是没有意义的,所以局部成员都不能使用static修饰,
在构造器中使用this引用时,this总是引用该构造器正在初始化的对象。
方法本身是指令的操作码部分,保存在stack中,方法内部变量作为指令的操作数部分,跟着指令的操作码之后,保存在stack中(实际是简单类型保存在stack中,引用类型在stack中保存地址,在heap中保存值)。
对象实例以及非静态属性保存在heap中的,而heap必须通过stack中的地址指针才能够被指令(类的方法)访问到。静态属性保存在stack中(这是网上找到的说,笔者认为应该是静态属性的地址保存在stack中)。
非静态方法有一个隐含的传入参数,该参数是JVM给的,和我们的代码无关,这个隐含参数就是对象实例在stack中的地址指针(this)。因此非静态方法(在stack中的指令代码)总是可以找到自己专用数据(在heap中的对象属性值)。当然非静态方法也必须获得该隐含参数,因此在调用非静态方法之前,必须先new一个对象实例,获得stack中地址的指针,否则JVM将无法将隐含参数传给非静态方法。而静态方法无需此隐含参数,因此不需要new对象,只要class文件被ClassLoader load进入JVM的stack,该方法即可被调用,当然此时静态方法存取不到heap中的对象属性的。
形成长度可变的方法
public void test(int a, String… books){}
可以传入多个字符串参数作为参数值,其实是可以看着参数数组。但一个方法只能有一个长度可变的形成且位于形参列表的最后。
方法重载
方法的重载要求:两同(同一个类中的方法名相同),一不同(参数列表不同)(不建议使用长度可变的形参重载方法),跟方法的其他部分(返回值类型(有时我们调用方法时不需要返回值,就不能根据返回值类型来确定到底是调用哪个方法),修饰符等)没有任何关系。
系统在第一次使用类加载类,并初始化类。类属性从这个类准备阶段起开始存在。系统不会为局部变量执行初始化,局部变量在访问之前一定要确定是已经初始化。
模块设计追求高内聚(尽可能把模块的内部数据,功能实现细节隐藏在模块内部独立完成,不允许外部直接干预),低耦合(仅暴露少来的方法给外部使用)
构造器
如果我们提供了自定义的构造器,系统不再提供默认的构造器,通常我们都保留无参数的默认构造器。
多态性
当编译时类型和运行时类型不一致,就会出现所谓的多态。
当把子类对象赋给父类引用变量时,被称为向上转型,这个总是可以成功的,但把一个父类对象赋给一个子类引用变量时,就需要强制类型转换,而且还可能在运行时产生ClassCastException异常(当这个父类对象编译时类型为父类类型,运行时类型是子类类型才是正确的),使用instanceof运算符可以让强制类型转换更安全。
Instanceof 运算符
a instanceof B a必须是具体的实例,B是一种类(或接口),可以是数组
如果A是编译时能够确定具体的类型,那么Instanceof 运算符前面操作数的编译时类型要么与后面类(接口,抽象类)相同,要么是后面类的父类,(即可以通过类型转换(B)a到B的,不能通过(B)a转换到B的可以,转换到它们共有的父类如(Object)a,但仍返回false),否则会引起编译错误。如果要运行时才确定类型(如何调用函数getObject()返回对象等,但new String()这个在编译时就会确定下类型,因为String Integer等类被final修饰,所以编译时就确定类型),没有这样的限定。
B不能是确定的泛型参数的泛型,可以是 List或者 List<?>
Instanceof 运算符前面的对象是否是后面的类,或者其子类。实现类的实例。如果是,就返回true,否则返回false,若a是null则返回false。
经过instanceof 运算符判断一个对象是否可以强制类型转换,然后在使用(type)运算符进行强制转换,从而保证程序不会出现错误。
初始化块,构造器质性顺序
当Java创建一个对象时,系统先为该对象的所有实例属性分配内存(前提是该类已经被加载过了),接着程序开始对这些实例属性执行初始化,其初始化顺序是:先执行初始化块或声明属性是指定的初始化值,然后执行构造器里指定的初始值。
类初始化阶段:(类初始化只执行依次,然后会在虚拟机一直存在)
先执行最顶层父类的静态初始化块,依次向下,最后执行当前类的静态初始化块
对象初始化阶段:(每次创建实例对象是都要进行)
先执行最最顶层父类的初始化块,构造器,依次向下,最后执行当前类初始化块。
构造器。
初始化块和声明指定初始化值,他们的执行顺序与源代码中的排列顺序相同。
基本类型之间的转换
Xxx parseXxx(String s) 将字符串类型转换成基本类型(除了Character之外)
String 中的valueOf() 将基本类型转换成字符串。
==和equals
== 判断两个变量是否相等是,如果2个变量是基本类型的变量,且都是数值型(不一定要求数据类型严格形同),则只要两个变量值相等,就返回true;但当判断两个引用类型的变量,必须它们指向同一个对象是,== 判断才会返回true。
equals 在比较字符串是只要两个字符串引用变量的指向内容相等就返回true(String类已经重写了Object的equals方法),自定义的类要想达到这样的效果,必须重写Object的equals方法(自定义标准),否则用这个方法判断两个对象相等的标准与 == 符号是没有区别的。
单例类(Singleton)
一个类只能创建一个实例
eg:
class Singleton
{
private static Singleton instance;
//隐藏构造器
private Singleton(){}
//提供一个静态方法,用于返回Singleton实例
//该方法可以加入自定义的控制,保证只产生一个Singleton对象
public static Singleton getInstance()
{
if(instance==null)
{
Instance=new Singleton();
}
return instance;
}
}
这样用 getInstance返回的对象始终都是同一个。
final 修饰符
当final修饰引用类型变量时,final只保证这个引用所引用的地址不会改变即一直引用同一个对象,但这个对象可以发生改变。
如果final修饰的变量是基本数据类型,且在编译时就可以确定该变量的值,于是可以把该变量当成常量处理,如果是修饰引用数据类型,就无法在编译时就获得值,而必须在运行时才能得到值。
成员变量是随着类初始化或对象初始化而初始化的。当类初始化时,系统会为该类的类属性分配内存,并分配默认值;当创建对象时,系统会为该对象的和私立属性分配内存,并分配默认值。
类属性:可在静态初始化块中,声明该属性时指定初始值
实例属性:可在非静态初始化块中,声明该属性、构造器中指定初始值
而且上面初始化操作要其只能被初始化一次。(必须由程序员显示初始化)
如果在构造器,初始化块中对final成员变量进行初始化,则不要在初始化之前访问final成员变量的值,否则会出现“可能未初始化变量”的错误。(系统不会进行隐式初始化)。
final 修饰的方法只是不能被重载,并不是不能被重写。
抽象类
抽象方法不能有方法体 abstract void String getName();
只要含有抽象方法(直接定义了一个抽象方法;继承了一个抽象父类,但没有完全实现父类包含的抽象方法;以及实现一个接口,但没有完全实现接口包含的抽象方法)的类就是抽象类
抽象类可以包含普通类的成分,但是必须有抽象方法。
abstract 不能用于修饰属性和构造器,private 和 abstract 不能同时使用。
更彻底的抽象:接口
修饰符可以是public或者省略,省略了public 访问控制符,则只有在相同包结构下才可以访问该接口。
一个接口可以有多个直接父接口,但不能继承类。
一个接口不能包含构造器和初始化块定义,可以包含属性(只能是常量),方法(只能是抽象方法),内部类,内部接口,和枚举类定义。
定义接口成员是,可以省略访问控制修饰符,如果指定访问修饰符,只能使用public修饰符。在接口定义属性时,不管是否使用 public static final修饰符,接口里的属性总是使用者三个修饰符来修饰。同样接口总是使用public abstract 来修饰。接口不可使用static修饰接口里定义的方法。同理接口总是使用public static对内部类(静态内部类),内部接口和枚举类进行修饰。
内部类
内部类相当外部类定义的一个普通方法,可以访问外部类的私有数据和方法。当内部类和外部类的成员或者方法出现重名时,可以使用this和外部类名.this来区分。外部类就相当一个命名空间。
非静态内部类
非静态内部类对象必须寄存在外部类对象里,故要创建一个非静态内部类一定要有通过外部了对象。在静态方法中不能直接通过new 内部类了创建内部类对象(没有外部类对象)(类似静态方法不能调用非静态方法以及访问非静态属性)。
非静态内部类里不能有静态方法,静态属性,静态初始化块,可以有普通初始化块。
静态内部类
静态内部类相当于一个外部类的一个静态方法。
内部类的使用
Out.In in=new Out().new In();
当继承内部类是一定要有外部类对象来调用内部类的构造器。
局部内部类
局部内部类无需使用访问控制阀和static修饰符修饰
匿名内部类
匿名内部了不能是抽象类,不能定义构造器,因为匿名内部类没有类名,所以无法定义构造器,但可以定义实例初始化块。
枚举类(省略)
泛型
当使用泛型接口、父类时不能再包含类型形参,如继承带泛型的父类、接口要传入世界参数类型。 如 public class A extends B<String>。
ArrayList<String>并没有生产新的类,系统不会生产真正的泛型类,如应用 instance instanceof List<String> 编译时引起错误:instanceof 运算符后不能使用泛型类。
List<String>对象不能被当成List<Object>对象使用,也就是说List<String>类并不是List<Object>类的子类。如 ArrayList<Number> nList=new ArrayList<Integer>(); 会引起编译错误。
这点与数组不同,如果Apple 是 Fruit 的子类,则 Apple[] 依然是 Fruit[]的子类
类型通配符
为了表示各种List的父类,使用类型通配符“?”,但是并不能把元素加入到其中(否则会引起编译错误),因为我们并不知道元素的类型,唯一例外的是null,它是所有引用类的实例。其次,我们使用get()方法返回List<?>集合指定索引的元素,其返回值是一个未知类型,当可以肯定的是,它是一个Object类型。前面我们使用List<E> 使用add 的参数必须是E类的对象或者是子类的对象。
设定通配符的上限
List<? Extends Fruit> 同样此处我们使用add参数类型是 ?extends Fruit 它表示 Fruit未知的子类,我们无法准确知道这个类型是什么,所以我们无法将任何对象添加到这种集合中。
设定类型形参的上限
class A <T extends Fruit>
更极端 class A<T extends Number &java.io.Serializable>
泛型方法
public <T >void add (T a, List<T> list)
{
t.add(a);
}
如果无需向泛型集合添加元素,或者修改泛型集合的元素时,可以使用类型通配符,无需使用泛型方法。
设定通配符的下限
List<? super Type> 它表示它必须是Type本身或者Type的父类。
泛型的擦除和转换
擦除,当把一个具有泛型形象的对象赋给另一个没有泛型信息的变量时,则所有在尖括号直接的类型信息都被扔掉,如List<String> 类型被转换成List,则该List对几乎元素的类型检查变成了类型变量的上限(即Object),当再次 String s =list.get(0);将会引起编译错误(要强制转换)。
转换,
List<Integer> li=new ArrayList<Integer>();
li.add(6);
//编译运行正常,只是引起“未经检查的转换”的警告
List list=li; //泛型擦除
List<String> ls=list;
System.out.println(ls.get(0)); //将引起运行时异常
//同样会引起运行时异常,不能进行类型转换
System.out.println((String)li.get(0));
List<?> lis=li;
System.out.println(li.get(0)); //OK
Lis.add(new Integer(6)); //error
泛型数组
数组元素的类型不能包含类型变量或类型形参,除非是无上限的类型通配符。
List<String>[] la=new List<String>[10]; //这是不允许的
但可以
List<String> la=new ArrayList[10]; //编译时有“未经检查的转换”警告
使用时候注意不要 引起“ClassCastException”异常。
异常处理
进行异常捕获的时候一定要先捕获小的异常,然后在捕获大的异常。
在finally块中使用了return或throw的语句,将会导致try块、catch块中的return,throw 语句失效。
当java程序执行try块、catch块时遇到return throw语句(没有System.exit(1);语句),这两个语句或导致该方法立即结束,所以系统并不会立即执行这个两个语句,而是去寻找该异常处理流程中是否包含finally块,如果有finally块,只有当finally块执行完毕,系统才会再次跳回来执行try块、catch块里的return或throw语句。
数组与内存控制
数组静态初始化:
String[] books=new String[]{“wo shi “,”zhongshan”,”daxue”};
String[] books={“wo shi “,”zhongshan”,”daxue”};
数组动态初始化:
String[] books=new String[5];
执行动态初始化时系统将为数组元素分配默认初始值。
不要同时使用静态初始化和动态初始化。
通过其他数组赋值初始化:
String[] books1=books;
数组变量是一种引用类型的变量(存储的是数组的引用),数组变量本身存储在栈区,指向的内容是指向堆区。在Java中对象的引用本身不需要进行初始化,而对象本身才需要进行初始化。
所有局部变量都是放在各自的方法栈内存里保存的,但引用类型变量所引用的对象则总是存储在堆内存中的。只要程序通过引用类型变量访问属性或者调用方法,则该引用类型将被其所引用的对象代替,即要进行初始化。
int[] sArra=null;
System.out.println(sArra) //这是没有问题的,输出的是null
对象与内存控制
实例变量和类变量
static不能修饰外部类,局部变量,局部内部类
//下面代码将提示:非法向前引用
int num1=num2+1;
int num2=3;
上面代码变量同时用 static 修饰同样会出现上述问题。
但
//下面代码正常
int num1=num2+1;
static int num2=3;
这说明类变量的初始化时间总是比实例变量之前。
使用javap –c 类名 可以得到该类初始化的过程,当我们队一个类初始化过程不是很清楚的时候不妨尝试下该命令。
实例变量的初始化时机
每次创建Java对象的时都会为实例变量分配内存空间,并对实例变量执行初始化
实例变量初始化有三种方式
定义实例变量时指定初始值;
非静态初始化块中对实例变量指定初始值;
构造器中对实例变量指定初始值
其实前两种初始化执行顺序和它们在源代码中位置排列相同,也就拥有想多等级初始化时机,但都比第三种初始化方式更早。
实际中实例变量初始化 指定初始值和初始化块 当经过编译器处理后,它们都会被提取到构造器中。
例如:
double a=24;
实际上会被分成如下2步执行:
(1)double a; 创建Java对象时系统根据该语句为该对象分配内存
(2)a=24; 这条语句是被提取到Java类的构造器中执行
public class JavapToolTest
{
//初始化快中为count实例变量指定初始值
count = 12;
}
int count = 20;
//定义构造器
public JavapToolTest()
{
System.out.println(count);
}
使用javap –c 命令可以得到如下代码:结果就一目了然了
C:\Users\D.S.Qiu\Desktop\codes\codes\02\2.1>javap -c JavapToolTest
Compiled from "JavapToolTest.java"
public class JavapToolTest {
int count;
public JavapToolTest();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":
()V
4: aload_0
5: bipush 12
7: putfield #2 // Field count:I
10: aload_0
11: bipush 20
13: putfield #2 // Field count:I
16: getstatic #3 // Field java/lang/System.out:Ljava/
io/PrintStream;
19: aload_0
20: getfield #2 // Field count:I
23: invokevirtual #4 // Method java/io/PrintStream.printl
n:(I)V
26: return
类变量初始化时机
类变量初始化的方法
定义类变量时指定初始值;
静态初始化块中对类变量指定初始值
初始化时机与实力变量初始化时机类似。
对没有给出初始化的变量系统总分配默认值。
父类构造器
当创建Java对象时,程序总会先依次调用每个父类的非静态初始化块,父类构造器执行初始化,最后才调用本类的非静态初始块,构造器执行初始化。
隐式调用和显示调用
如果子类构造器中没有使用super调用父类构造器,将会隐式调用父类无参数的构造器。
super 和this调用构造器最多只能使用其中一者,而且必须作为构造器代码的第一行。
访问子类对象的实例变量
下面看一个极端的例子
class Base
{
//定义了一个名为i的实例变量
private int i = 2;
public Base()
{
this.display();
}
public void display()
{
System.out.println(i);
}
}
//继承Base的Derived子类
class Derived extends Base
{
//定义了一个名为i的实例变量
private int i = 22;
//构造器,将实例变量i初始化为222
public Derived()
{
i = 222;
}
public void display()
{
System.out.println(i);
}
}
public class Test
{
public static void main(String[] args)
{
//创建Derived的构造器创建实例
new Derived();
}
}
最终输出的竟是 0 。实际上,构造器只是负责对Java对象实例变量执行初始化(赋初始值),但在执行构造器之前,该对象所占的内存已经被分配下来了,只是默认初始值(空值)。
程序在执行Derived类构造器之前首先会执行父类Base构造器(当然Object类在此之前是一定会被执行),此时Base中的 i 已经指定初始值2,然后执行构造器中的 this.display(); 关键是此处this 代表谁,是Base还是Derived
为此我们可以在Base类构造器中添加如下代码:
System.out.println(this.i);
System.out.println(i);
其实结果都是输出 2,但在Derived的display()函数中添加上述代码,输出结果都是0,这表明访问成员变量只有所处的类有关,但为什么调用this.display();却输出 0 。
其实我们知道:this在构造器中代表正在初始化的Java对象,而我们易知此时正在初始化对象时Derived对象——是Derived()构造器隐式调用了Base构造器的代码,故得出this代表Derived对象,而不是Base对象。我们可以用System.out.println(this.class)来验证我们的结论。
但由于上述this位于Base的构造器中的,所以该this虽然代表的是Derived对象但是它的编译类型仍然是Base,只是实际引用了一个Derived对象(运行时类型)。
我们知道当变量编译时类型和运行时类型不一致时,通过该变量访问其引用对象的实例属性(成员变量)时,该实例属性的值由访问该属性的所处的类决定(不论是否使用this),但调用实例方法时,该方法行为是由它的实际引用的对象类型(运行时类型)决定。
此外,如果我们在Derived类中定义了一个getI()方法 然后在Base类的构造器中调用this.getI()方法时会报错,因为this的编译类型是Base类型不能调用getI()方法而无法通过编译。
调用子类重写的方法
参考下面程序代码
class Animal
{
//desc实例变量保存对象toString方法的返回值
private String desc;
public Animal()
{
//调用getDesc()方法初始化desc实例变量
this.desc = getDesc(); //(3)
}
public String getDesc()
{
return "Animal";
}
public String toString()
{
return desc;
}
}
public class Wolf extends Animal
{
//定义name、weight两个实例变量
private String name;
private double weight;
public Wolf(String name , double weight)
{
//为name、weight两个实例变量赋值
this.name = name; //(3)
this.weight = weight;
}
//重写父类的getDesc()方法
@Override
public String getDesc()
{
return "Wolf[name=" + name + " , weight="
+ weight + "]";
}
public static void main(String[] args)
{
System.out.println(new Wolf("灰太郎" , 32.3)); //(1)
}
}
当然是先初始化Wolf对象,在执行(2)之前先调用父类的无参数构造函数,this.desc=getDesc(); 执行getDesc()函数根据前面,我们知道调用的是子类Wolf的方法,但此时Wolf的name和weight还没有执行初始化(只是默认空值),故会输出 Wolf[name=null,weight=0];
为了避免这种问题的发生,我们可以这样定义Animal的toString()方法
class Animal
{
public String getDesc()
{
return “Animal”;
}
public String toString()
{
return getDesc();
}
}
这样就子类Wolf调用父类toString()的方法时,只要重写getDesc()方法就可以得到想要的结果。
父子实例的内存控制
继承成员变量和继承方法的区别参照如下代码
class Base
{
int count = 2;
public void display()
{
System.out.println(this.count);
}
}
class Derived extends Base
{
int count = 20;
@Override
public void display()
{
System.out.println(this.count);
}
}
public class FieldAndMethodTest
{
public static void main(String[] args)
{
Derived d=new Derived();
//声明一个Base变量,并将Derived对象赋给该变量
Base bd = new Derived();
//直接访问count实例变量和通过display访问count实例变量
System.out.println(bd.count);
bd.display();
//让d2b变量指向原d变量所指向的Dervied对象
Base d2b = d;
//访问d2b所指对象的count实例变量
System.out.println(d2b.count);
System.out.println(((Derived)d2b).count);
d2b.display();
}
}
输出结果是 2 20 2 20 20
其实我知道 d ,db, d2b实际指向的是一个Derived对象,而这个对象分别存储了父类和自己的count的值。
此外 如果我们敲下:Derived dd=(Derived)new Base();运行时会出现类型转换错误。(编译可以通过,运行时会报错)。
另外,我们通过javap –c 命令可以知道 编译器在处理(继承下来的)方法和变量时的区别,对于父类的变量子类不会有保留,这使得子类可以拥有和父类同名的实例变量(通过对象名直接访问属性时是由编译类型决定,在函数内访问与属性所处的类名有关),但如果子类重写了父类的方法,就意味着子类定义的方法彻底覆盖了父类的同名方法了(调用函数是由运行时类型决定)。
内存中子类的实例
上面程序中 Derived d=new Derived();
Base d2b=d;
其实就只创建了一个Derived对象,但是这个Derived对象不仅保存了在Derived类中定义的所有实例变量,还保存了它所有父类所定义的全部实例变量。
对于this,我想总结一个更为深刻的结论就是,this到底代表什么对象跟运行时哪个对象执行this所在的方法,而与this所在的方法位置没有直接的关系。Java可以通过return this 返回调用当前方法的Java对象(子类调用父类的方法(若方法返回值是return this,则返回的是子类对象),但不允许直接return super 来返回父类对象。Super关键字本身没有引用任何对象,不允许将super直接当做变量使用如 super==a 是会引起编译错误,在子类使用super只是作为调用父类方法的限定(当子类重写了父类的相应方法)。
父、子类的类变量
由于类变量属于类本身,因此不会像实例变量那样复杂。
final修饰符(一定要显式指定初始值,不能默认空值)
final 修饰的实例变量必须是显式指定初始值,只能在3个位置指定初始值
定义final实例变量时指定初始值
在非静态初始化块中为final实例变量指定初始值
在构造器中为final实例变量指定初始值
本质上final实例 变量只能在构造器中初始值(用javap –c 可以查看响应的原理)
对于final 修饰的类变量只能在 定义时和静态初始化块指定初始值,其实 经过编译器的处理,这2种方式都会被抽取到静态初始化块去赋予初始值。
final修饰局部变量一定要在定义时指定初始值。
final 修饰引用类型变量时,引用变量本身不能改变,但其引用的对象可以改变。
执行“宏替换”的变量
final变量在指定初始值之后本质是已经不再是一个变量,而是相当一个直接量。
public class Test
{
public static void main(String[] args)
{
//定义一个普通局部变量
int a = 5;
System.out.println(a);
}
}
使用javap -c Test 编译
C:\Users\D.S.Qiu\Desktop\codes\codes\02\2.4>javap -c Test
Compiled from "FinalLocalTest.java"
public class Test {
public Test();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":
()V
4: return
public static void main(java.lang.String[]);
Code:
0: iconst_5
1: istore_1
2: getstatic #2 // Field java/lang/System.out:Ljava/
io/PrintStream;
5: iload_1
6: invokevirtual #3 // Method java/io/PrintStream.printl
n:(I)V
9: return
}
如果使用final修饰变量a,使用java –c分析新产生的class文件发现缺少了 istore_1 和iload_1两行代码,这说明用final修饰的变量a之后,变量a就完全消失了。当定义final变量时指定了初始值,而这初始值可以在编译时就确定下来了,那这个final变量本质上就是一个“宏变量”,编译器会把程序中所有用到该变量的地方直接替换成该变量的值。
Java会缓存所有曾经用过的字符串直接量
String s1=”疯狂”;
String s2=”Java”;
String s3=”疯狂”+”Java”;
String s4=s1+s2;
System.out.println(s1==s2); //输出的是false
因为编译器无法在编译时确定s2的值,不会让s3指向字符串池中的缓存中的“疯狂Java”,故s3==s4输出false。
如果我们将s1,s2用final去修饰,s1,s2就变成“宏变量”这样在编译时就可以确定s4的值,就可以获得true的输出。
值得注意的是,final变量只有在定义该变量指定初始值,才会出现“宏替换”的效果。
对于类变量的final修饰类似。
Java要求所以内部类访问的局部变量都要使用final修饰那是有原因的:对于局部变量而言,作用域就只停留在该方法内,当方法执行结束,该局部变量也要随着消失,但如果内部类如果产生隐式的“闭包”,闭包将使得局部变量脱离它的所在方法的继续存在(如下面的程序)。是故如果不可避免要使用该变量可以将该变量声明成全局变量。
public class ClosureTest
{
public static void main(String[] args)
{
//定义一个局部变量
final String str = "Java";
//在内部类里访问局部变量str
new Thread(new Runnable()
{
public void run()
{
for (int i = 0; i < 100 ; i++ )
{
//此处将一直可以访问到str局部变量
System.out.println(str + " " + i);
//暂停0.1秒
try
{
Thread.sleep(100);
}
catch (Exception ex)
{
ex.printStackTrace();
}
}
}
}).start();
//执行到此处,main方法结束
}
}
常见Java集合的实现细节
Set和Map 参看源代码
HashMap和HashSet
对于HashSet而言,系统采用Hash算法决定集合元素的存储位置,而HashMap 系统将value当成key的附属物。
HashMap类有一个重要的内部接口Map.Entry,每个Map.Entry其实就是一个key-value对。当两个Entry对象的key的hashCode()返回值相同时,将有key通过eqauls()比较值决定是采取覆盖行为(返回true)还是产生Entry链。
当创建hashMap是将initialCapacity参数值指定为2的n次方,这样可以减少系统的计算开销。
对于HashSet而言,它是基于HashMap实现的。HashSet底层采用HashMap来保存所有的元素,而其的value则存储了一个PRESENT,它是一个静态的Object对象。重写equals()和hashCode()方法必须保持返回值的一致性,所有计算hashCode返回值的关键属性,都应该用于作为equals比较的标准。
TreeMap和TreeSet
TreeSet底层采用的一个 NavigableMap来保存TreeSet集合的元素。但实际上,由于NavigableMap只是一个接口,因此底层依然是使用TreeMap来包含set集合中的所有元素。
Map和List
Map的values()方法
HashMap的values()方法表明上返回一个Values集合对象,但这个对象不能添加元素,主要用于遍历HashMap里的所有value。
ArrayList 和LinkedList
ArrayList和ArrayQeque(是Deque集合的实现类)底层都是基于Java数组实现的,Vector(还有一个Stack子类)其实就是ArrayList的线程安全版本,绝大部分实现方法都是相同的,只是在方法上增加了synchronized修饰。
LinkedList是一种链式存储的线性表,本质就是双向链表,不仅实现了List接口,还实现了Deque(双端队列)接口。
Java的内存回收
都说Java不会内存泄露,但是在一定情况还是会发生的。例如我们对一个Stack结构按如下方式定义一个pop方法
修改Stack的size属性,也就是记录栈内元素减1
返回elementData数组中索引为size-1的元素
这样是会导致内存泄露的(没有做相应赋null处理)。
内存管理小技巧
尽量使用直接量
使用StringBuilder和StringBuffer进行字符串连接
尽早释放无用对象的引用
尽量少用静态变量
避免在经常调用的方法、循环体重创建java对象(每次创建都要分配内存空间,执行初始化操作,然后就是回收操作,影响程序的性能)。
缓存经常使用的对象
尽量不要使用finalize方法(将资源清理放在finalize方法中完成是非常拙劣的选择,垃圾回收机制工作量比较大)。
表达式中的陷阱
Java程序创建对象的常规方式有
通过new调用构造器创建Java对象
通过Class对象的newInstance()方法调用构造器来创造Java对象
通过Java反序列化机制从IO流中回复Java对象
通过Java对象提供的clone方法复制一个新的对象
String s=”疯狂”+”Java”+10;
String s1=”疯狂Java10”;
s==s1; //true
上述表达式中如果运算数都是字符串直接量,整数直接量,没有变量参与(有“宏替换”例外),没有方法调用,JVM在编译时就确定了该字符串连接的表达式的值。
String s=”疯狂”+”Java”+”DSQiu”;
上述字符串只创建了一个对象,因为str的值可以在编译时确定下来,JVM会在编译时就计算出s的值为”疯狂JavaDSQiu”,然后将该字符串直接量放入字符串池中,并不存在所谓的”疯狂”等字符串对象。
表达式类型的陷阱
表达式的类型自动提升
当表达式中有多个基本类型的知识,整个算术表达式的数据类型自动提升与表达式中最高级别操作数同样
char
int long float double 从左向右提升类型
byte short
short sVa=5;
sVa=sVa-2; //无法通过编译表达式右边提升成 int类型
但若改为 sVa-=2;则没有问题,因为复合赋值运算符包含了隐式类型转换。但隐式转换类型可能会出现“截断”误差(数据溢出) 如 short st=5; st+=90000; st却得到24479.
用 +=连接字符串是 +=左边的变量只能是String类型,不能是String的父类型。
int a=23;
int b=a/2; //除不尽,但是没有问题,由于a是int类型的,a/2表达式也依然保持int类型。
输入法的陷阱
当我们输入空格时,使用的是中文输入法的空格,编译的时候会报出“非法字符”的错误。
注释字符必须合法
如 G:\codes\uinits5\Hello.java 该句注释会报出“非法转义字符”的错误,Java允许直接使用\uxxxx的形式表示字符,必须后是0~F的字符。
转义字符的陷阱
如 System.out.println(“abc\u000a”.length());
\u000a是一个换行符,而字符串中是不允许直接使用换行符(要转移)或者说字符串不能分成两行来书写故编译时报出错误。
同样//\u000a也会引起错误
然而 http://www.sysu.edu.cn 这个直接出现在代码中是没有错误的,因为可以看成http:(标签 goto)和//www.sysu.edu.cn(注释)
泛型可能引起的错误
看代码
List list=new ArrayList();
list.add(“sb”);
List<Integer> inList=list; //编译时只会有warning
System.out.println(inList.get(0)); //没有问题能正常输出字符串,访问集合元素是不关心集合实际类型
Integer in=inList.get(0); //编译时正常,运行时访问集合元素当实际类型与集合所带的泛型信息不匹配时抛出ClassCastException
String s=inList.get(0); //编译不能通过,不能兼容类型
此外,当把一个带泛型信息的Java对象付给不带泛型信息的变量是,Java程序不仅会擦除传入的类型参数的泛型信息,还会擦除其他泛型信息(如 public List<String> getValues()中返回值及括号的信息。
Class Apple<t extends Number> 擦除信息后编译器只能知道上限是Number但不知道具体是哪个子类。
不能创建泛型数组 如 List<String>[] list=new List<String>[10]; //不被允许的
面向对象的陷阱
Instanceof运算符的陷阱
Instanceof运算符前面操作数的编译时类型必须是一下3中情况
与后面的类相同
是后面的类父类
是后面的类子类
如果它实际引用的对象是第二个操作数的实例,或者是第二个操作数的子类,实现类(接口等)的实例,运算结果返回true。
在编译阶段,强制转换要求被转换变量的编译时类型必须是以下3中情况
与目标的类相同
是目标的类父类 返回false
是目标的类子类(自动向上转型) 返回true
运行阶段,被转型变量所引用对象的实际类型必须是目标类型的实例,或者是目标类型的子类,实现类的实例,无论是否强制转换或者编译时类型是什么:
Base base=new Derived();
Derived d=new Derived();
Base base1=new Base();
//Base是Derived的父类
System.out.println(base1 instanceof Derived); //false
System.out.println(d instanceof Base); //ture
System.out.println((Base)base instanceof Derived); //true
System.out.println((Derived)base instanceof Base); //true
null可以作为所以引用类型的变量的值,所以在使用instanceof之前要排除null的影响。
构造器的陷阱
反序列化机制恢复的对象虽然和原来对象具有完全相同的实例变量值,但是系统中会将会禅师两个对象。使用反序列化机制对单例类机制的破坏,可以为单例类提供readResolve方法(直接返回静态实例变量)就可以解决。
通过重写clone 方法实现对Java对象的复制也需要调用构造器。
无限递归的构造器
防止构造器的递归调用
尽量不要在定义实例变量是指定实例变量的值为当前类的实例
尽量不要初始化块中创建当前类的实例
尽量不要在构造器内调用本构造器创建Java对象
调用哪个重载方法
一个方法被多次重载后,传入参数可能被向上转型,使之符合调用方法的需要;JVM会选择一个最精确匹配的一个方法。
看代码
public void info(Object obj,int count)
public void info(Object[] obj, double count)
当调用 info(null,5)会引起“引用不明确”的异常,null与Object[]更匹配,5与int更匹配。
非静态内部类的陷阱
系统在编译阶段总会为非静态内部类的构造器增加一个参数(第一个参数总是外部类)(可以通过javap –c分析获得),因此调用非静态内部类的构造器是必须传入一个外部类对象作为参数,否则程序将引发运行时异常(不能通过反射调用newInstance()来创建内部类)。
非静态内部类(本身相当于一个非静态上下文)当然不能拥有静态成员。
this.super() 不能正常使用可以用 类名.this.super()来解决。该类名不是当前类的类名
异常捕捉的陷阱
对于Check异常,要么声明抛出,要么使用try…catch进行捕捉。
finally回收资源安全模式
finally
{
If(os!=null)
{
try{
os.close();
}catch(Excepiton e){}
}
}
学过Java基础的人都能很容易理解上面的代码和多态的原理,但是仍有一些关键的地方需要注意的,算是自己对多态的一个小结:
1.Java中除了static和final方法外,其他所有的方法都是运行时绑定的。在我另外一篇文章中说到private方法都被隐式指定为final的,因此final的方法不会在运行时绑定。当在派生类中重写基类中static、final、或private方法时,实质上是创建了一个新的方法。
2.在派生类中,对于基类中的private方法,最好采用不同的名字。
3.包含抽象方法的类叫做抽象类。注意定义里面包含这样的意思,只要类中包含一个抽象方法,该类就是抽象类。抽象类在派生中就是作为基类的角色,为不同的子类提供通用的接口。
4.对象清理的顺序和创建的顺序相反,当然前提是自己想手动清理对象,因为大家都知道Java垃圾回收器。
5.在基类的构造方法中小心调用基类中被重写的方法,这里涉及到对象初始化顺序。
6.构造方法是被隐式声明为static方法。
7.用继承表达行为间的差异,用字段表达状态上的变化。
我一直有个这样的想法:如果我们每个人都乐于分享自己学习经验,那么我们在自学的时候能够少走很多弯路,顺利找到捷径。因此我希望能够有一套这样的分享机制:每个人都可以帮自己的学习经验整理出来,供大家学习交流,并能做进一步补充,不断完善,最终趋于完美。
Email:[email protected]