数组与内存控制
数组是静态的,被初始化后,长度不可变。初始化就是为数组对象分配内存,并指定初始值。
初始化两种方式:
静态:初始化时程序员显式指定每个元素的初始值,由系统决定长度。
动态:程序员只指定长度,系统分配初始值。
Java的数组变量是引用类型的变量,数组变量并不是数组本身,它指向堆内存中的数组对象。
数组不一定要初始化,可以将数组变量指向一个有效的已有数组。
所以的局部变量都是放在栈内存中的,不管其是基本类型的变量或者引用类型的变量。都是存储在各自的方法栈内。但引用类型变量所引用的对象,则总是指向堆内存中。而堆内存中的对象通常不许直接访问。
多维数组可先初始化最左边的维数,此时该数组的每个元素相当于一个数组引用变量。
对象与内存控制
实例变量和类变量
局部变量存于栈内存,可分为三类:
形参,方法内的局部变量,代码块内的局部变量。
Static只能修饰类的成员,不能修饰外部类,不能修饰局部变量、局部内部类。
Java定义成员变量时,必须采用合法的前向引用。
Int num1=num2+2;
Int Num2=20;//错误
但是,如果一个是实例变量,一个是类变量,那么实例变量总是可以引用类变量。
Int num1=num2+2;
Static int num2=20;
class Cat
{
String name;
int age;
public Cat(String n,int a)
{
System.out.println("执行构造器");
name=n;
age=a;
}
{
System.out.println("执行非静态初始化块");
weight=2.0;
}
double weight=2.3;
public String toString(){return "Cat[name= "+name
+", age= "+age+", weight= "+weight+"]";
}
}
public class Test
{
public static void main(String[] args){
Cat cat=new Cat("Kitty",2);
System.out.println(cat);
cat=new Cat("Jerfield",3);
System.out.println(cat);
}
}
同一个JVM中,每个类只对应一个Class对象,但每个类可以创建多个java对象。
实例变量的初始化时机:1、定义时执行初始值 2、非静态初始化中指定初始值 3、构造器中指定初始值。最终编译器处理后,都会合并到构造器中。
每次程序调用构造器来创建java对象时,构造器获得执行机会,除此之外,该类的非静态初始化块将获得执行机会,且总在构造器执行之前。定义变量时指定的初始值和初始化块中的指定的初始值的执行顺序,与它们的排列顺序相同。
类变量的初始化时机:1、定义时 2、静态初始化块
class Price
{
final static Price INSTANCE=new Price(2.8);
static double initPrice=20;
double currentPrice;
public Price(double discount){currentPrice=initPrice-discount;}
}
class Test
{
public static void main(String[] args){
System.out.println(Price.INSTANCE.currentPrice);
Price p=new Price(2.8);
System.out.println(p.currentPrice);
}
}
-2.8,17.2
父类构造器
程序初始化步骤:
1、执行父类非静态初始化块
2、隐式或显式调用父类的一个或多个构造器执行初始化
3、执行自身累非静态初始化块
4、隐式或显式调用自身的一个或多个构造器执行初始化
class Base
{
private int i=2;
public Base(){this.display();}
public void display(){System.out.println(i);}
}
class Derived extends Base
{
private int i=22;
public Derived(){i=222;} //2
public void display(){System.out.println(i);}
}
public class Test
{
public static void main(String[] args){ new Derived();} //1
}
输出0。
1处创建Derived对象时,系统为该对象分配内存空间,它拥有两个i对象,均为0。(构造器只是负责java对象实例的初始化,在这之前,该对象所占的内存已被分配)。程序在执行Derived构造器之前,先执行Base构造器,先将Base类中的i实例赋值为2,再调用this.display()。但是,这里的this代表Derived!若改为:
public Base(){System.out.println(this.i);this.display();}输出2和0。
public Base(){System.out.println(this.getClass());} 输出为Derived类,但是它编译使类型是Base类。
当变量的编译时类型和运行时类型不同时,通过该变量访问它引用的对象的实例变量,该实例变量的值由声明该变量的类型决定。但通过该变量调用它引用的对象的实例方法时,该方法行为由它实际引用的对象来决定。
如果父类构造器调用了被子类重写的方法,且通过子类构造器来创建子类对象,调用了这个父类构造器,就会导致子类的重写方法在子类构造器之前执行,从而导致子类的重写方法访问不到子类的实例变量值。
父子实例的内存控制
如果子类重写了父类方法,则覆盖,对于实例变量不存在这种现象。
Super关键字本身没有任何引用对象,它甚至都不能被当成一个真正的引用对象使用:
Ø 子类方法不能直接使用return super,但可使用return this
Ø 程序不许直接把super当变量使用
子类定义与父类同名的实例变量不会完全覆盖父类定义的实例变量,它只是隐藏,仍会分配内存空间。
Final修饰符
Final的实例变量必须显式指定初始值:
Ø 定义时指定
Ø 非静态初始化块中
Ø 构造器中
这三种方式都会被抽取到构造器中赋初始值。
Final的类变量只能在两个地方指定初始值:
Ø 定义时
Ø 静态初始化块中
本质上都是在静态代码块中被赋初始值的。
Final变量的初始值在编译期就确定下来,本质上是一个宏变量,在用到该变量的地方,直接替换成该变量的值。
final String book="疯狂Java讲义:"+99.0;
final String book2="疯狂Java讲义:"+String.valueOf(99.0);
System.out.println(book=="疯狂Java讲义:99.0");
//book2因为调用了方法,无法在编译时确定下来。
System.out.println(book2=="疯狂Java讲义:99.0");
True,false。
String s1="疯狂Java";
String s2="疯狂"+"Java";
System.out.println(s1==s2);
//true,s2在编译时就被确定为“疯狂Java”,直接指向字符串
//池的字符串
String str1="疯狂";
String str2="Java";
String s3=str1+str2;
System.out.println(s1==s3);
//false,s3在编译器无法确定。
若将str1和str2均改为final,则输出true。
对于普通类变量,定义位置不影响效果。对于final类变量,只有在定义final类变量时指定初始值,系统才会对该变量执行宏替换。
如果要在匿名内部类、普通内部类中使用外部类的局部变量,那么必须用final修饰。
集合和容器
Set和map
Map(关联数组)的所有key集中起来就是一个set。对于map而言,相当于每个元素都是key-value的set集合。
Java集合实际上是多个引用变量组成的集合,这些引用变量指向实际的java对象。
public HashSet() {
ap = new HashMap<E,Object>();
}
1、HashSet底层是采用HashMap实现的。
private transient HashMap<E,Object> map;是HashSet类里面定义的一个私有的成员变量。并且是transient类型的,在序列化的时候是不会序列化到文件里面去的,信息会丢失。HashMap<E,Object>里面的key为E,是HashSet<E>里面放置的对象E(对象的引用,为了简单方便起见我说成是对象,一定要搞清楚,在集合里面放置的永远都是对象的引用,而不是对象,对象是在堆里面的,这个一定要注意),HashMap<E,Object>的value是Object类型的。
2、这个HashMap的key就是放进HashSet中对象,value是Object类型的。当我去使用一个默认的构造方法的时候,执行public HashSet() {map = new HashMap<E,Object>();},会将HashMap实例化,将集合能容纳的内型作为key,Object作为它的value。当我们去往HashSet里面放值的时候,这个HashMap<E,Object>里面的Object类型的value到底取什么值呢?这个时候我们就需要看HashSet的add方法,因为add方法可以往里面增加对象,通过往里面增加对象,会导致底层的HashMap增加一个key和value。这个时候我们看一下add方法: public boolean add(E e) {return map.put(e, PRESENT)==null;},add方法接受一个E类型的参数,这个E类型就是我们在HashSet里面能容纳的类型,当我们往HashSet里面add对象的时候,HashMap是往里面put(E,PRESET),增加E为key,PRESET为value,PRESET是这样定义的:private static final Object PRESENT = new Object();从这里我们知道PRESET是private static final Object的常量并且已经实例化好了。HashMap<E,Object>()中的key为HashSet中add的对象,不能重复,value为PRESET,这是jdk为了方便将value设为常量PRESET,因为value是可以重复的。
3、当调用HashSet的add方法时,实际上是向HashMap中增加了一行(key-value对),该行的key就是向HashSet增加的那个对象,该行的value就是一个Object类型的常量。
4、HashMap底层采用数组来维护。数组里面的每一个元素都是Entry,而这个Entry里面是Key和value组成的内容。
5、调用增加的那个对象的hashCode方法,来得到一个hashCode值,然后根据该值来计算出一个数组的下表索引(计算出数组中的一个位置)
6、将准备增加到map中的对象与该位置上的对象进行比较(equals方法),如果相同,那么就将该位置上的那个对象(Entry类型)的value值替换掉,否则沿着该Entry的链接继承重复上述过程,如果到链的最后仍然没有找到与此对象相同的对象,那么这个时候就会将该对象增加到数组中,将数组中该位置上的那个Entry对象链到该对象的后面。
7、对于HashSet,HashMap来说,这样做就是为了提高查找的效率,使得查找时间不随着Set或者Map的大小而改变。
TreeMap采用“红黑树”来保存每个map的Entry,每个Entry是一个节点。效率比hashmap低。
Map和List
Map不会保存key加入的顺序。这些key可组成Set集,另一组是value集合。Value可重复,map可根据key来获取value。
List集合也包含两个值,一组是虚拟的int类型索引,临沂市List集合元素。List相当于key都是int的map。
ArrayList和LinkedList
Stack采用Vector实现。Vector只有一个writeObject方法,并未完全实现定制序列化,序列化角度看来,ArrayList的实现比Vector简单。除此之外,Vector就是ArrayList的线程安全版本。
内存回收
引用种类
Jvm采用有向图方式来管理内存中的对象,可解决循环引用问题。一个对象在有向图的状态分为:
Ø 可达状态 有引用变量指向的对象
Ø 可恢复状态 如果某个对象不再有任何变量引用,先进入可恢复状态,finalize函数可以使其再次可达。
Ø 不可达状态
Java语言对对象的引用有四种:
Ø 强引用
程序创建一个对象,并把这个对象赋给一个引用变量。这时处于可达状态,不可悲垃圾回收机制回收。
Ø 软引用
通过SoftReference类实现,它可能被回收。内存充足时,与强引用没有太大区别,不足时,可能被回收。
Ø 弱引用
与软引用累死,区别在于弱引用的引用对象的生存期更短。通过WeakReference实现,当回收机制运行时,不管内存是否足够,都会回收该对象占用的内存。
Ø 虚引用
虚引用不可单独使用,它的主要作用是跟踪对象被垃圾回收的状态,程序可以通过检查与虚引用关联的引用队列是否包含指定的虚引用,来了解虚引用所引用的对象是否被回收。引用队列由java.lang.ref.ReferenceQueue类表示,用于保存被回收后对象的引用。虚引用在对象释放之前,会把它对应的虚引用添加到它关联的引用队列,这使得可以在对象被回收之前采用行动。虚引用采用PhantomReference实现,完全类似于没有引用。
内存泄漏
如果程序有些对象,处于可达状态,但程序永远不再访问它们,它们占用的空间不会被回收,它们占用的空间也会内存泄漏。
垃圾回收
垃圾回收的工作:
Ø 跟踪并监控每一个java 对象,当某个对象不可达,即回收它占用的内存
Ø 清理内存分配、回收过程产生的碎片
垃圾回收机制不可能实时监控每个java对象状态,因此一个对象失去引用后,不会被立即回收,只有等垃圾回收运行时才会被回收。
基本算法
设计垃圾回收的算法,有以下可供选择的设计:
Ø 串行回收和并行回收
串行就是始终只用一个CPU执行垃圾回收。并行把回收任务拆分成多部分,每个部分由一个CPU负责。后者复杂,副作用:内存碎片
Ø 并发执行和应用程序停止
并发要解决和应用程序冲突的问题
Ø 压缩和不压缩和复制
支持压缩的垃圾回收期会把获得对象搬到一起,然后之前占用的内存全部回收。
基本算法:
Ø 复制
Ø 标记清除-即不压缩
Ø 标记压缩
垃圾回收期用分代的方式来设计不同的回收设计,将堆内存分为3个代:
Ø 年轻代 Young
年轻代的大部分对象很快就会进入不可达状态,因而采用复制算法。年轻代有一个Eden区和2个Survivor区构成。绝大部分对象先分配到Eden区,有些大的对象会被直接分配大老年代,Survivor区中的对象都至少在年轻代中经历以此垃圾回收。所以这些对象在被转移到老年代之前先保留在Survivor空间中。同一时间2个Survivor空间,一个用来保存对象,另一为空,用来下次垃圾回收时保存年轻代中的对象。每次复制就是将Eden和第一个Survivor的可达对象复制到第2个Survivor,然后清空。
Ø 老年代 Old
老年代垃圾回收的频率不高,但完成需要更长时间。采用标记压缩算法
Ø 永久代 Permanent
主要用来装载class、方法等信息,默认64M。垃圾回收机制通常不会回收永久代对象。
内存管理小技巧
Ø 尽量使用直接量,少用new 来创建
Ø 使用StringBuilder和StringBuffer来进行字符串连接
Ø 尽早释放无用对象,置为null
Ø 尽量少用静态变量
Ø 避免在经常调用的方法和循环中创建java对象
Ø 缓存经常使用的对象,例如采用Hashmap
Ø 尽量不要使用finalize方法
Ø 考虑使用SoftReference
表达式中的陷阱
字符串的陷阱
String java=new String(“crazy java”);
该语句一共创建了两个String对象,一个”crazy java”直接量的字符串对象,另一个是由new String创建的字符串对象。
Java穿件对象的四种方式:
Ø New调用构造器
Ø Class对象的newInstance调用构造器
Ø Java的反序列化来从IO流中恢复java对象
Ø Clone方法
另外,基本类型的包装类,允许以直接量的方式来创建对象。
对于字符直接量,JVM会用一个字符串池来保存它们,一般情况下,不会进行内存回收。也可以通过字符串连接表达式来创建对象,如果该表达式的值可以在编译时确定,那么JVM在编译时计算变量的值,并让它指向字符串池中的字符串。而如果使用了变量、或是调用了方法,只能等到运行期猜确定。如果字符连接运算的所有变量都可执行宏替换,那么也可以在编译期确定。
String str=”hello”+”java”+”haha”;//只创建了一个对象。
System提供的identityHashCode方法用来获取某个对象唯一的hashCode值,与是否重写了hashCode无关。
StringBuffer相比于StringBuilder,是线程安全的。
字符串是否相同,用==。比较它们包含的字符序列是否相同,用equals函数。另外,String类实现了Comparable接口,可以用Compareto方法。
表达式类型的陷阱
表达式类型的自动提升
一个算术表达式包含多个基本类型时:
Ø 所以byte、short、char都会提升为int
Ø 整个算术表达式的数据类型自动提升到与表达式右侧最高等级操作数的类型
Short s=6;
S=s-2;//int妄想赋给short
而s-=2;就没问题。复合运算符包含了隐式转化。这个潜在的危险会导致结果的高位被截断。
E1 op=E2不等价于E1= E1 op E2,而是等价于E1=(E1)(E1 op E2)
System.out.println("Hello!"+'a'+7);//Hello!a7
System.out.println('a'+7+"Hello!");//104Hello!
Java程序中通常不可包含全角字符,但字符串中可以,注释中也可以。
注释中不可包含Unicode转义字符。
转义字符的陷阱
Java对Unicode转义字符不会做任何特殊的处理,只是简单替换。
System.out.println(“abc\u000a”.length());
相当于System.out.println(“abc
“.length());
泛型可能引发的错误
Ø 当程序把一个原始类型的变量赋给一个带泛型信息的变量时,只要它们的类型保持兼容即可。例如将List变量赋给List<Integer>
Ø 当程序试图访问带泛型声明的集合的集合元素时,编译器总是把集合元素当成泛型类型来处理,不关系集合元素的实际类型。
Ø 当程序试图访问带泛型声明的集合的集合元素时,JVM会遍历每个元素自动执行强制转型,如果元素的实际类型与集合所带的类型不匹配,则运行时有ClassCastException。
当一个带泛型信息的java对象赋给不带泛型信息的变量时,java程序会发生擦除使用该java类时传入的类型实参,也会擦除所有的泛型信息。
JDK不许创建泛型数组。
正则表达式陷阱
String.split(String regex)参数是正则表达式。.号可以匹配任意字符。
多线程陷阱
不要调用run方法
三种创建和启动线程方式:Thread,Runnable,Callable(重写call方法)
继承Thread效果最差:单继承,多线程之间共享数据麻烦
静态的同步方法
Synchronized(this),this作为同步监视器。
流程控制陷阱
Switch代码
只有当switch语句的前面分支没有获得执行时,default才会被执行。在每个case后,都有一个break。
Switch的表达式,只能是:Byte,short,int,char,enum
标签
http://www.google.com
if陷阱
if();
循环体的花括号
For,while,do循环的重复执行语句不能是一条单独的局部变量定义语句,必须加花括号。
For陷阱
final int START=999999999;
for(float i=START;i<START+50;i++)
System.out.println("i的值:"+i+" "+new java.util.Date());
一直输出1.0E9
final int START=999999999;
for(float i=START;i<START+20;i++)
System.out.println("i的值:"+i+" "+new java.util.Date());
没有任何输出
因为
Float f1=999999999;
F1=f1+20;
F1!=f1+50;
Foreach
List<Integer> Li=new ArrayList<Integer>();
Li.add(1);
Li.add(2);
Li.add(3);
for(int i:Li){
i=4;
System.out.println(i);
}
输出4
循环计数器只是一个中间变量,报错了当前正在遍历的元素,不会改变集合元素本身。
面向对象的陷阱
Instanceof
InstanceOf运算符前面的编译时类型必须是
Ø 与后面的类相同
Ø 是后面类的父类
Ø 是后面类的子类
Java的强制转型,可分为编译、运行两个阶段:
Ø 编译阶段,强制转型要求被转型的变量的编译时类型必须是
ü 被转型变量的编译时类型与目标类型相同
ü 被转型变量的编译时类型是目标类型父类
ü 被转型变量的编译时类型是目标类型子类,这是可自动向上转型
Ø 运行阶段,被转型变量所引用对象的实际类型必须是目标类型的实例,或者是目标类型的子类、实现类的实例,否则抛出ClassCastException。
Object obj="Hello";
String objStr=(String)obj;//ibj编译期是Object,String的父类;obj变量实际上也是String,运行时也正常
System.out.println(objStr);
Object objPri=new Integer(5);
String str=(String)objPri;//objPri变量的变异类型是Object,可以强制转换。obj实际是Integer,运行时抛出ClassCastException
String s="Crazy Java";
Math m=(Math)s;//String不是math子类或父类,编译错误
String s=null;
System.out.println("null是否是String类型的实例:"+(s instanceof String));
打印false
构造器陷阱
构造器之前的void
构造器不能声明返回值类型,也不能使用void声明构造器没有返回值。
构造器创建对象吗
构造器并不创建对象,只是负责初始化。无需使用构造器的情况:
Ø 反序列化恢复java对象
Ø Clone方法复制java对象
无限递归的构造器
Ø 尽量不要在定义实例变量时指定实例变量的值为当前类的实例
Ø 尽量不要在初始化中创建当前类的实例
Ø 尽量不要再构造器中调用本构造器创建java对象
持有当前类的实例
一个实例变量持有当前类的另一实例变量是被允许的。但要小心
到底调用了哪个重载的方法
调用方法时传入的实际参数可能被向上转型。Java的重载解析过程分为以下两个阶段:
Ø 第一阶段JVM会选取所以可获得并匹配调用的方法货构造器
Ø 决定调用最精确的
class Test
{
public static void info(Object obj,int count){
System.out.println("obj参数为:"+obj);
System.out.println("count参数为:"+count);
}
public static void info(Object[] obj,double count){
System.out.println("objs参数为:"+obj);
System.out.println("count参数为:"+count);
}
public static void main(String[] args){
Test.info(null,5);
}
}
这个郁闷喽,两个都匹配。
方法重写的陷阱
重写private方法
子类无法访问父类的private方法,自然也就无法重写。
重写其他访问权限的方法
无法访问自然也就无法重写。
非静态内部类的陷阱
class Test
{
class Inner
{
public String toString(){return "Inner对象";}
}
public void test() throws Exception{
System.out.println(new Inner());
/*会将this作为实参传入Inner构造器*/
System.out.println(Inner.class.newInstance());
/*非静态内部类必须寄生在外部类的实例中,没有外
部类的对象,就不可能产生非静态内部类的对象,因
此非静态内部类不可能有无参数的构造器,即便是系
统提供的默认构造器,也许一个外部类对象*/
}
public static void main(String[] args)throws Exception{
new Test().test();
}
}
非静态内部类不能用拥有静态成员
非静态内部类的子类
class Out
{
class In
{
public void test(){System.out.println("In的test方法");}
}
class A extends In{ }
}
public class Test extends Out.In //错误: 需要包含Out.In的封闭实例
{
public static void main(String[] args){
System.out.println("Hello World!");
}
}
错误的关键在于,由于非静态内部类In必须寄生Out对象之内,因此父类Out.In根本没有无参数的构造器。系统会为提供一个无参的构造器。在Test无参的构造器内,编译器会增加代码super,执行调用父类Out.In无参的构造器,必然导致编译错误。
非静态内部类在外部以内派生子类是安全的。但也不绝对。
Static关键字
静态方法属于类
public class Test
{
public static void info(){
System.out.println("静态的info方法");
}
public static void main(String[] args){
Test t=null;
t.info();//虽然采用Test对象类调用,仍然实用类作为主调
}
}
class BaseTest
{
public static void info(){System.out.println("BaseTest的Info方法");}
}
public class Test extends BaseTest
{
public static void info(){
System.out.println("Test的info方法");
}
public static void main(String[] args){
BaseTest bt1=new BaseTest();
bt1.info();
BaseTest bt2=new Test();
bt2.info();
}
}
静态方法属于类,实际上调用都是通过BaseTest类调用。
静态内部类不能访问外部类的非静态成员。
Native方法的陷阱
Native方法只有方法签名,没有方法体。通常借助C语言来实现:
1. 用javah编译第一步生成的Class文件,产生一个.h文件
2. 写一个cpp文件实现native方法,其中需要包含第一步产生的.h文件
3. 将第2步的cpp编译成动态链接库文件
4. 在java中用System的loadLibrary()方法或Runtime的loadLibrary加载第3步产生的动态链接文件
Native方法无法做到跨平台。
异常捕捉的陷阱
正确关闭资源方式
finally{
if(oos!=null){
try{
oos.close();
}
catch(Exception ex){
ex.printStackTrace();
}
}
}
这才是正确的资源关闭方式,这保证三点:
1) 使用finally来关闭物理资源,保证关闭资源总会被执行
2) 关闭每个资源之前,首先保证不为空
3) 关闭资源时也可能异常,要捕捉
Finally块的陷阱
System.exit(0)被调用时,虚拟机退出前执行两项清理工作:
Ø 执行系统中注册的所有关闭钩子(要用Runtime.getRuntime.addShutdownHook())
Ø 如程序调用了System.runFinalizerOnExit(true),那么JVM会对所有还未结束的对象调用Finalizer
Finally块和方法返回值
public class Test
{
public static int test(){
int count=5;
try{
return ++count;
}finally{
return count++;
}
}
public static void main(String[] args){
int a=test();
System.out.println(a);
}
}
输出6
当程序执行try块、catch块遇到throw语句,throw语句会导致该方法立即结束,系统执行到throw语句不会立即抛出异常,而是先寻找是否有finally,如没有,抛出异常。如果有,立即执行finally,完后抛出异常。如果finally块里使用return语句来结束方法,系统不会跳回执行try块和catch块。
Catch块的用法
Try后使用catch块来捕获多个异常时,程序要小心多个catch的顺序:捕捉父类异常的catch块都该排在捕捉子类异常的catch之后。
不要用catch代替流程控制
程序难以阅读,运行速度缓慢
只能catch可能抛出的异常
IndexOutOfBoundException、NullPointerException两个异常都是RuntimeExcetion的子类,属于运行时异常,而IOException、ClassNotFoundException属于Checked异常。如果一个catch试图捕获一个XXXException的Checked异常,那么它对应的语句必须可能抛出XXXException或其子类。无论try怎么样,catch(Exception)总没错。
程序总是可以抛出catch捕获运行时异常。
继承得来的异常
一个子类的方法抛出的异常,只能是父类方法的交集。