文章目录
Java 基础
- Oracle JDK有部分源码是闭源的,如果确实需要可以查看OpenJDK的源码,可以在该网站获取。
- http://grepcode.com/snapshot/repository.grepcode.com/java/root/jdk/openjdk/8u40-b25/
- http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/file/73d5bcd0585d/src
- 上面这个还可以查看native方法。
1.1 JDK&JRE&JVM
-
JDK(Java Development Kit)是针对Java开发员的产品,是整个Java的核心,包括了Java运行环境JRE、Java工具(编译、开发工具)和Java核心类库。
-
Java Runtime Environment(JRE)是运行JAVA程序所必须的环境的集合,包含JVM标准实现及Java核心类库。
-
JVM是Java Virtual Machine(Java虚拟机)的缩写,是整个java实现跨平台的最核心的部分,能够运行以Java语言写作的软件程序。
-
JDK包含JRE和Java编译、开发工具;
-
JRE包含JVM和Java核心类库;
-
运行Java仅需要JRE;而开发Java需要JDK。
1.2 跨平台
- 字节码是在虚拟机上运行的,而不是编译器。
- 换而言之,是因为JVM能跨平台安装,所以相应JAVA字节码便可以跟着在任何平台上运行。只要JVM自身的代码能在相应平台上运行,即JVM可行,则JAVA的程序员就可以不用考虑所写的程序要在哪里运行,反正都是在虚拟机上运行,然后变成相应平台的机器语言,而这个转变并不是程序员应该关心的。
1.3 基础数据类型
-
第一类:整型 byte short int long
-
第二类:浮点型 float double
-
第三类:逻辑型 boolean(它只有两个值可取true false)
-
第四类:字符型 char
- byte(1)的取值范围为-128~127(-2的7次方到2的7次方-1)
- short(2)的取值范围为-32768~32767(-2的15次方到2的15次方-1)
- int(4)的取值范围为(-2147483648~2147483647)(-2的31次方到2的31次方-1)
- long(8)的取值范围为(-9223372036854774808~9223372036854774807)(-2的63次方到2的63次方-1)
- float(4)
- double(8)
- char(2)
- boolean(1/8)
这里很好理解 一个字节byte=八位bit, 而第一位是符号位,所以
byte是2^7
short是2^15
…
-
内码是程序内部使用的字符编码,特别是某种语言实现其char或String类型在内存里用的内部编码;
-
外码是程序与外部交互时外部使用的字符编码。“外部”相对“内部”而言;
不是char或String在内存里用的内部编码的地方都可以认为是“外部”。
例如,外部可以是序列化之后的char或String,或者外部的文件、命令行参数之类的。
-
Java语言规范规定,Java的char类型是UTF-16的code unit,也就是一定是16位(2字节),
然后字符串是UTF-16 code unit的序列。
Java规定了字符的内码要用UTF-16编码。
或者至少要让用户无法感知到String内部采用了非UTF-16的编码。 -
String.getBytes()是一个用于将String的内码转换为指定的外码的方法。无参数版使用平台的默认编码作为外码,有参数版使用参数指定的编码作为外码;将String的内容用外码编码好,结果放在一个新byte[]返回。调用了String.getBytes()之后得到的byte[]只能表明该外码的性质,而无法碰触到String内码的任何特质。
-
Java标准库实现的对char与String的序列化规定使用UTF-8作为外码。
Java的Class文件中的字符串常量与符号名字也都规定用UTF-8编码。
这大概是当时设计者为了平衡运行时的时间效率(采用定长编码的UTF-16)与外部存储的空间效率(采用变长的UTF-8编码)而做的取舍。
1.4 引用类型
- 类、接口、数组都是引用类型
四种引用
- 目的:避免对象长期占用内存,
强引用
- StringReference GC时不回收
- 当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。
软引用
- SoftReference GC时如果JVM内存不足时会回收
- 软引用可用来实现内存敏感的高速缓存。 软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。
弱引用
- WeakReference GC时立即回收
- 弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。
- 弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。
虚引用
- PhantomReference
- 如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。 虚引用主要用来跟踪对象被垃圾回收的活动。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。
- 在Java集合中有一种特殊的Map类型:WeakHashMap, 在这种Map中存放了键对象的弱引用,当一个键对象被垃圾回收,那么相应的值对象的引用会从Map中删除。WeakHashMap能够节约存储空间,可用来缓存那些非必须存在的数据。
基础数据类型包装类
为什么需要
- 由于基本数据类型不是对象,所以java并不是纯面向对象的语言,好处是效率较高(全部包装为对象效率较低)。
- Java是一个面向对象的编程语言,基本类型并不具有对象的性质,为了让基本类型也具有对象的特征,就出现了包装类型(如我们在使用集合类型Collection时就一定要使用包装类型而非基本类型),它相当于将基本类型“包装起来”,使得它具有了对象的性质,并且为其添加了属性和方法,丰富了基本类型的操作。
有哪些
基本类型 | 包装器类型 |
---|---|
boolean | Boolean |
char | Character |
byte | Byte |
short | Short |
long | Long |
float | Float |
double | Double |
Number 是所有数字包装类的父类
自动装箱、自动拆箱(编译器行为)
- 自动装箱:可以将基础数据类型包装成对应的包装类
Integer i = 10000;
// 编译器会改为new Integer(10000)
- 自动拆箱:可以将包装类转为对应的基础数据类型
int i = new Integer(1000);
//编译器会修改为 int i = new Integer(1000).intValue();
- 自动拆箱时如果包装类是null,那么会抛出NPE
Integer.valueOf
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
- 调用Integer.valueOf时-128~127的对象被缓存起来。
- 所以在此访问内的Integer对象使用==和equals结果是一样的。
- 如果Integer的值一致,且在此范围内,因为是同一个对象,所以返回true;但此访问之外的对象比较的是内存地址,值相同,也是返回false。
1.5 Object
== 与 equals的区别
- 如果两个引用类型变量使用==运算符,那么比较的是地址,
它们分别指向的是否是同一地址的对象。
结果一定是false,因为两个对象不可能存放在同一地址处。 - 要求是两个对象都不是能空值,与空值比较返回false。
- ==不能实现比较对象的值是否相同。
- 所有对象都有equals方法,默认是Object类的equals,其结果与==一样。
- 如果希望比较对象的值相同,必须重写equals方法。
hashCode与equals的区别
Object中的equals:
public boolean equals(Object obj) {
return (this == obj);
}
- equals 方法要求满足:
- 对于任意非空引用x,x.equals(null) 应该返回false
下面要求非空情况下
- 自反性 a.equals(a)
- 对称性 x.equals(y) y.equals(x)
- 传递性 x.equals(y) x.equals(z) 则 z.equals(y)
- 一致性 x.equals(y) 多次调用结果一致
Object中的hashCode:
public native int hashCode();
-
它是一个本地方法,它的实现与本地机器有关,这里我们暂且认为他返回的是对象存储的物理位置。
-
当equals方法被重写时,通常有必要重写hashCode方法,以维护hashCode方法的常规约定:值相同的对象必须有相同的hashCode。
- object1.equals(object2)为true,hashCode也相同;
- hashCode不同时,object1.equals(object2)为false;
- hashCode相同时,object1.equals(object2)不一定为true;
-
当我们向一个Hash结构的集合中添加某个元素,集合会首先调用hashCode方法,这样就可以直接定位它所存储的位置,若该处没有其他元素,则直接保存。若该处已经有元素存在,就调用equals方法来匹配这两个元素是否相同,相同则不存,不同则链到后面(如果是链地址法)。
-
先调用hashCode,唯一则存储,不唯一则再调用equals,结果相同则不再存储,结果不同则散列到其他位置。因为hashCode效率更高(仅为一个int值),比较起来更快。
-
HashMap#put源码
-
hash是key的hash值,当该hash对应的位置已有元素时会执行以下代码(hashCode相同)
// 如果equals返回结果相同,则值一定相同,不再存入。
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
如果重写equals不重写hashCode会怎样
两个值不同的对象的hashCode可能相同,那么执行equals如果结果为true,
HashSet或HashMap的键会放入值相同的对象,造成覆盖或替换。
1.6 String&StringBuffer&StringBuilder
- 都是final类,不允许继承;
- String长度不可变,StringBuffer、StringBuilder长度可变;
- StringBuilder 线程不安全,所以效率越高
- StringBuffer 线程安全
String
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {}
equals&hashCode
String重写了Object的hashCode和equals。
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
添加功能
-
String是final类,不可被继承,也不可重写一个java.lang.String(类加载机制)。
-
一般是使用StringUtils来增强String的功能。
-
为什么只加载系统通过的java.lang.String类而不加载用户自定义的java.lang.String类呢?
双亲委派机制
- 因加载某个类时,优先使用父类加载器加载需要使用的类。如果我们自定义了java.lang.String这个类,
- 加载该自定义的String类,该自定义String类使用的加载器是AppClassLoader,根据优先使用父类加载器原理,
- AppClassLoader加载器的父类为ExtClassLoader,所以这时加载String使用的类加载器是ExtClassLoader,
- 但是类加载器ExtClassLoader在jre/lib/ext目录下没有找到String.class类。然后使用ExtClassLoader父类的加载器BootStrap,
- 父类加载器BootStrap在JRE/lib目录的rt.jar找到了String.class,将其加载到内存中。这就是类加载器的委托机制。
- 所以,用户自定义的java.lang.String不被加载,也就是不会被使用。
package java.util;
/**
* 本类中的main方法不会正常执行,这里涉及到的知识点就是双亲委派逻辑
* 双亲委派的特点就是:
* 1. 父类存在则调用父类,不执行子类
* 2. 父类不存在,则执行子类
* 这里模拟的就是List类,这个类因为属于JDK自带的默认类,所以会先调用默认类;
* 而默认类中根本就没有main方法,所以执行本方法就报错了,这么做的好处就是防止
* 黑客模拟父类释放病毒等
* 但是如果是自定义的类则会出现当前的类覆盖类库的类的情况,更多详情看扩展阅读
*
*/
public class List {
public static void main(String[] args) {
System.out.println('a');
}
}
substring
- 会创建一个新的字符串(大于0才会创建新的字符串);
public String substring(int beginIndex, int endIndex) {
if (beginIndex < 0) {
throw new StringIndexOutOfBoundsException(beginIndex);
}
if (endIndex > value.length) {
throw new StringIndexOutOfBoundsException(endIndex);
}
int subLen = endIndex - beginIndex;
if (subLen < 0) {
throw new StringIndexOutOfBoundsException(subLen);
}
return ((beginIndex == 0) && (endIndex == value.length)) ? this
: new String(value, beginIndex, subLen);
}
public String(char value[], int offset, int count) {
if (offset < 0) {
throw new StringIndexOutOfBoundsException(offset);
}
if (count <= 0) {
if (count < 0) {
throw new StringIndexOutOfBoundsException(count);
}
if (offset <= value.length) {
this.value = "".value;
return;
}
}
// Note: offset or count might be near -1>>>1.
if (offset > value.length - count) {
throw new StringIndexOutOfBoundsException(offset + count);
}
this.value = Arrays.copyOfRange(value, offset, offset+count);
}
字符拼接
-
编译时会将+转为StringBuilder的append方法。
-
注意新的字符串是在运行时在堆里创建的。
-
String str1 = “ABC”;可能创建一个或者不创建对象,
如果”ABC”这个字符串在java String池里不存在,
会在java String池里创建一个创建一个String对象(“ABC”),然后str1指向这个内存地址,无论以后用这种方式创建多少个值为”ABC”的字符串对象,
始终只有一个内存地址被分配,之后的都是String的拷贝,Java中称为字符串驻留
,所有的字符串常量都会在编译之后自动地驻留。所以一般拼接字符串通常使用 StringBuilder 之类 或者String.format 或者 日志框架自带的
{}
尽量避免+ + +
这类 -
注意只有字符串常量是共享的,+和substring等操作的结果不是共享的,substring也会在堆中重新创建字符串。
####intern源码解读
-
String#intern(JDK1.7之后)
-
JDK1.7之后JVM里字符串常量池放入了堆中,之前是放在方法区。
-
intern()方法设计的初衷,就是重用String对象,以节省内存消耗。
-
一定是new得到的字符串才会调用intern,字符串常量没有必要去intern。
-
当调用 intern 方法时,如果池已经包含一个等于此 String 对象的字符串(该对象由 equals(Object) 方法确定)
则返回池中的字符串。
否则,常量池中直接存储堆中该字符串的引用(1.7之前是常量池中再保存一份该字符串)。
public native String intern();
实例一
String s = new String("1"); //生成了常量池中的“1” 和堆空间中的字符串对象。
s.intern(); //这一行的作用是s对象去常量池中寻找后发现"1"已经存在于常量池中了。
String s2 = "1"; //这行代码是生成一个s2的引用指向常量池中的“1”对象。
System.out.println(s == s2);// 结果就是 s 和 s2 的引用地址明显不同。因此返回了false。
//这行代码在字符串常量池中生成“1” ,并在堆空间中生成s3引用指向的对象(内容为"11")
// 注意此时常量池中是没有 “11”对象的。
String s3 = new String("1") + new String("1");
// 这一行代码,是将 s3中的“11”字符串放入 String 常量池中,此时常量池中不存在“11”字符串,
// JDK1.6的做法是直接在常量池中生成一个 "11" 的对象。
// 但是在JDK1.7中,常量池中不需要再存储一份对象了,可以直接存储堆中的引用
// 这份引用直接指向 s3 引用的对象,也就是说s3.intern() ==s3会返回true。
s3.intern();
// 这一行代码会直接去常量池中创建,但是发现已经有这个对象了
// 此时也就是指向 s3 引用对象的一个引用。因此s3 == s4返回了true。
String s4 = "11";
System.out.println(s3 == s4);// true
实例二
// 这行代码在字符串常量池中生成“1” ,并在堆空间中生成s3引用指向的对象(内容为"11")。
// 注意此时常量池中是没有 “11”对象的。
String s3 = new String("1") + new String("1");
// 这一行代码会直接去生成常量池中的"11"。
String s4 = "11";
// s3.intern(),这一行在这里就没什么实际作用了。因为"11"已经存在了。
// 结果就是 s3 和 s4 的引用地址明显不同。因此返回了false。
s3.intern();
System.out.println(s3 == s4);// false
实例三
String str1 = new String("SEU") + new String("Calvin");
System.out.println(str1.intern() == str1);// true
System.out.println(str1 == "SEUCalvin");// true
str1.intern() == str1就是上面例子中的情况,
str1.intern()发现常量池中不存在“SEUCalvin”,
因此指向了str1。 "SEUCalvin"在常量池中创建时,也就直接指向了str1了。
两个都返回true就理所当然啦。
实例四
String str2 = "SEUCalvin";//新加的一行代码,其余不变
String str1 = new String("SEU") + new String("Calvin");
System.out.println(str1.intern() == str1);// false
System.out.println(str1 == "SEUCalvin");// false
- str2先在常量池中创建了“SEUCalvin”,那么str1.intern()当然就直接指向了str2,
你可以去验证它们两个是返回的true。
后面的"SEUCalvin"也一样指向str2。
所以谁都不搭理在堆空间中的str1了,所以都返回了false。
总结如下
String intern() {
if(常量池找到该字面量的字符串) {
return 常量池该字面量的字符串
}
if(常量池已经记录字符串字面量相等的引用) {
return A
}
记录 堆上第一个与该字符串字面量相等的引用
return 记录的引用
}
StringBuffer StringBuilder
- StringBuffer是线程安全的,StringBuilder不是线程安全的,但它们两个中的所有方法都是相同的。StringBuffer在StringBuilder的方法之上添加了synchronized,保证线程安全。
- StringBuilder比StringBuffer性能更好。
常量池
String str = new String(“ABC”);
至少创建一个对象,也可能两个。因为用到new关键字,肯定会在heap中创建一个str2的String对象,它的value是“ABC”。
同时如果这个字符串在字符串常量池里不存在,会在池里创建这个String对象“ABC”。
String s1= “a”;
String s2 = “a”;
// 此时s1 == s2 返回true
String s1= new String(“a”);
String s2 = new String(“a”);
// 此时s1 == s2 返回false
//创建的字符串在字符串池中。
// 如果引号中字符串存在在常量池中,则仅在堆中拷贝一份(new String);
//如果不在,那么会先在常量池中创建一份("abc"),然后在堆中创建一份(new String),共创建两个对象。
堆和栈
Java把内存分成两种:
- 一种叫做栈内存
- 一种叫做堆内存
在函数中定义的一些基本类型的变量和对象的引用变量都是在函数的栈内存中分配。
当在一段代码块中定义一个变量时,java就在栈中为这个变量分配内存空间,
当超过变量的作用域后,java会自动释放掉为该变量分配的内存空间,
该内存空间可以立刻被另作他用。
堆内存用于存放由new创建的对象和数组。
在堆中分配的内存,由java虚拟机自动垃圾回收器来管理。
在堆中产生了一个数组或者对象后,还可以在栈中定义一个特殊的变量,
这个变量的取值等于数组或者对象在堆内存中的首地址,
在栈中的这个特殊的变量就变成了数组或者对象的引用变量,
以后就可以在程序中使用栈内存中的引用变量来访问堆中的数组或者对象,
引用变量相当于为数组或者对象起的一个别名或者代号。
引用变量是普通变量,定义时在栈中分配内存,引用变量在程序运行到作用域外释放。
而数组&对象本身在堆中分配,
即使程序运行到使用new产生数组和对象的语句所在地代码块之外,
数组和对象本身占用的堆内存也不会被释放,数组和对象在没有引用变量指向它的时候,
才变成垃圾,不能再被使用,但是仍然占着内存,
在随后的一个不确定的时间被垃圾回收器释放掉。
这个也是java比较占内存的主要原因,实际上,栈中的变量指向堆内存中的变量,
这就是 Java 中的指针!
java中内存分配策略及堆和栈的比较
- 内存分配策略
按照编译原理的观点,程序运行时的内存分配有三种策略,分别是静态的
栈式的
堆式的
静态存储分配是指在编译时就能确定每个数据目标在运行时刻的存储空间需求,因而在编译时就可以给他们分配固定的内存空间.这种分配策略要求程序代码中不允许有可变数据结构(比如可变数组)的存在,也不允许有嵌套或者递归的结构出现,因为它们都会导致编译程序无法计算准确的存储空间需求.
栈式存储分配也可称为动态存储分配,是由一个类似于堆栈的运行栈来实现的.和静态存储分配相反,在栈式存储方案中,程序对数据区的需求在编译时是完全未知的,只有到运行的时候才能够知道,但是规定在运行中进入一个程序模块时,必须知道该程序模块所需的数据区大小才能够为其分配内存.和我们在数据结构所熟知的栈一样,栈式存储分配按照先进后出的原则进行分配。
静态存储分配要求在编译时能知道所有变量的存储要求,栈式存储分配要求在过程的入口处必须知道所有的存储要求,而堆式存储分配则专门负责在编译时或运行时模块入口处都无法确定存储要求的数据结构的内存分配,比如可变长度串和对象实例.堆由大片的可利用块或空闲块组成,堆中的内存可以按照任意顺序分配和释放.
- 堆和栈的比较
上面的定义从编译原理的教材中总结而来,除静态存储分配之外,都显得很呆板和难以理解,下面撇开静态存储分配,集中比较堆和栈:
从堆和栈的功能和作用来通俗的比较,堆主要用来存放对象的,栈主要是用来执行程序的.而这种不同又主要是由于堆和栈的特点决定的:
在编程中,例如C/C++中,所有的方法调用都是通过栈来进行的,所有的局部变量,形式参数都是从栈中分配内存空间的。实际上也不是什么分配,只是从栈顶向上用就行,就好像工厂中的传送带(conveyor belt)一样,Stack Pointer会自动指引你到放东西的位置,你所要做的只是把东西放下来就行.退出函数的时候,修改栈指针就可以把栈中的内容销毁.这样的模式速度最快, 当然要用来运行程序了.需要注意的是,在分配的时候,比如为一个即将要调用的程序模块分配数据区时,应事先知道这个数据区的大小,也就说是虽然分配是在程序运行时进行的,但是分配的大小多少是确定的,不变的,而这个"大小多少"是在编译时确定的,不是在运行时.
堆是应用程序在运行的时候请求操作系统分配给自己内存,由于从操作系统管理的内存分配,所以在分配和销毁时都要占用时间,因此用堆的效率非常低.但是堆的优点在于,编译器不必知道要从堆里分配多少存储空间,也不必知道存储的数据要在堆里停留多长的时间,因此,用堆保存数据时会得到更大的灵活性。事实上,面向对象的多态性,堆内存分配是必不可少的,因为多态变量所需的存储空间只有在运行时创建了对象之后才能确定.在C++中,要求创建一个对象时,只需用 new命令编制相关的代码即可。执行这些代码时,会在堆里自动进行数据的保存.当然,为达到这种灵活性,必然会付出一定的代价:在堆里分配存储空间时会花掉更长的时间!这也正是导致我们刚才所说的效率低的原因,看来列宁同志说的好,人的优点往往也是人的缺点,人的缺点往往也是人的优点(晕~).
- JVM中的堆和栈
JVM是基于堆栈的虚拟机.JVM为每个新创建的线程都分配一个堆栈.也就是说,对于一个Java程序来说,它的运行就是通过对堆栈的操作来完成的。堆栈以帧为单位保存线程的状态。JVM对堆栈只进行两种操作:以帧为单位的压栈和出栈操作。
我们知道,某个线程正在执行的方法称为此线程的当前方法.我们可能不知道,当前方法使用的帧称为当前帧。当线程激活一个Java方法,JVM就会在线程的 Java堆栈里新压入一个帧。这个帧自然成为了当前帧.在此方法执行期间,这个帧将用来保存参数,局部变量,中间计算过程和其他数据.这个帧在这里和编译原理中的活动纪录的概念是差不多的.
从Java的这种分配机制来看,堆栈又可以这样理解:堆栈(Stack)是操作系统在建立某个进程时或者线程(在支持多线程的操作系统中是线程)为这个线程建立的存储区域,该区域具有先进后出的特性。
每一个Java应用都唯一对应一个JVM实例,每一个实例唯一对应一个堆。应用程序在运行中所创建的所有类实例或数组都放在这个堆中,并由应用所有的线程共享.跟C/C++不同,Java中分配堆内存是自动初始化的。Java中所有对象的存储空间都是在堆中分配的,但是这个对象的引用却是在堆栈中分配,也就是说在建立一个对象时从两个地方都分配内存,在堆中分配的内存实际建立这个对象,而在堆栈中分配的内存只是一个指向这个堆对象的指针(引用)而已。
Java 中的堆和栈
Java把内存划分成两种:一种是栈内存,一种是堆内存。
在函数中定义的一些基本类型的变量和对象的引用变量都在函数的栈内存中分配。
当在一段代码块定义一个变量时,Java就在栈中为这个变量分配内存空间,当超过变量的作用域后,Java会自动释放掉为该变量所分配的内存空间,该内存空间可以立即被另作他用。
堆内存用来存放由new创建的对象和数组。
在堆中分配的内存,由Java虚拟机的自动垃圾回收器来管理。
在堆中产生了一个数组或对象后,还可以在栈中定义一个特殊的变量,让栈中这个变量的取值等于数组或对象在堆内存中的首地址,栈中的这个变量就成了数组或对象的引用变量。
引用变量就相当于是为数组或对象起的一个名称,以后就可以在程序中使用栈中的引用变量来访问堆中的数组或对象。
具体的说:
栈与堆都是Java用来在Ram中存放数据的地方。与C++不同,Java自动管理栈和堆,程序员不能直接地设置栈或堆。
Java的堆是一个运行时数据区,类的(对象从中分配空间。这些对象通过new、newarray、anewarray和multianewarray等指令建立,它们不需要程序代码来显式的释放。堆是由垃圾回收来负责的,堆的优势是可以动态地分配内存大小,生存期也不必事先告诉编译器,因为它是在运行时动态分配内存的,Java的垃圾收集器会自动收走这些不再使用的数据。但缺点是,由于要在运行时动态分配内存,存取速度较慢。
栈的优势是,存取速度比堆要快,仅次于寄存器,栈数据可以共享。但缺点是,存在栈中的数据大小与生存期必须是确定的,缺乏灵活性。栈中主要存放一些基本类型的变量(,int, short, long, byte, float, double, boolean, char)和对象句柄。
栈有一个很重要的特殊性,就是存在栈中的数据可以共享。假设我们同时定义:
int a = 3;
int b = 3;
编译器先处理int a = 3;
首先它会在栈中创建一个变量为a的引用,然后查找栈中是否有3这个值,
如果没找到,就将3存放进来,然后将a指向3。
接着处理int b = 3;
在创建完b的引用变量后,
因为在栈中已经有3这个值,
便将b直接指向3。这样,
就出现了a与b同时均指向3的情况。
这时,如果再令a=4;
那么编译器会重新搜索栈中是否有4值,
如果没有,则将4存放进来,并令a指向4;
如果已经有了,则直接将a指向这个地址。
因此a值的改变不会影响到b的值。
要注意这种数据的共享与两个对象的引用同时指向一个对象的这种共享是不同的,
因为这种情况a的修改并不会影响到b, 它是由编译器完成的,它有利于节省空间。
而一个对象引用变量修改了这个对象的内部状态,会影响到另一个对象引用变量
编译优化
Java 编译期
可能包含了以下几点:
- 前端编译器(如 Javac)把 *.java 文件编译成 *.class 文件的过程
- 程序运行期的即时编译器(JIT 编译器,Just In Time Compiler)把字节码文件编译成机器码的过程
- 静态提前编译器(AOT 编译器,Ahead Of Time Compiler)直接把 *.java 文件编译成本地机器码的过程
对于很多新版本的语法特性被称为语法糖
, 在编译期间均会代替成基本的语法 , 如:
-
范型类型擦除
-
自动装拆箱
-
foreach循环
-
变长参数
-
条件编译(这一条其实算是优化代码结构,会将不可达分支清除,多条件合并)
-
字面量,final 都会在编译期被优化,并且会被直接运算好。
-
1)注意c和d中,final变量b已经被替换为其字符串常量了。
- 2)注意f、g中,b被替换为其字符串常量,并且在编译时字符串常量的+运算会被执行,返回拼接后的字符串常量
- 3)注意j,a1作为final变量,在编译时被替换为其字符串常量
-
解释 c == h / d == h/ e== h为false:c是运行时使用+拼接,创建了一个新的堆中的字符串ab,与ab字符串常量不是同一个对象;
-
解释f == h/ g == h为true:f编译时进行优化,其值即为字符串常量ab,h也是,指向字符串常量池中的同一个对象;
1.7 面向对象
抽象类与接口
-
区别:
- 1)抽象类中方法可以不是抽象的;接口中的方法必须是抽象方法;
- 2)抽象类中可以有普通的成员变量;接口中的变量必须是 static final 类型的,必须被初始化 , 接口中只有常量,没有变量。
- 3)抽象类只能单继承,接口可以继承多个父接口;
- 4)Java8 中接口中会有 default 方法,即方法可以被实现。
-
使用场景:
-
如果要创建不带任何方法定义和成员变量的基类,那么就应该选择接口而不是抽象类。
-
如果知道某个类应该是基类,那么第一个选择的应该是让它成为一个接口,只有在必须要有方法定义和成员变量的时候,才应该选择抽象类。因为抽象类中允许存在一个或多个被具体实现的方法,只要方法没有被全部实现该类就仍是抽象类。
三大特性
- 面向对象的三个特性:
封装
继承
多态
- 封装:将数据与操作数据的方法绑定起来,隐藏实现细节,对外提供接口。
- 继承:代码重用;可扩展性
- 多态:允许不同子类对象对同一消息做出不同响应
多态的三个必要条件:继承、方法的重写、父类引用指向子类对象
重写和重载
根据对象对方法进行选择,称为分派
- 编译期的静态多分派:overloading重载 根据调用引用类型和方法参数决定调用哪个方法(编译器)
- 运行期的动态单分派:overriding 重写 根据指向对象的类型决定调用哪个方法(JVM)
1.8 关键类
ThreadLocal(线程局部变量)
在线程之间共享变量是存在风险的,有时可能要避免共享变量,使用ThreadLocal辅助类为各个线程提供各自的实例。
public static final SimpleDateFormat sdf = new SimpleDateFormat(“yyyy-MM-dd”);
例如有一个静态变量,如果两个线程同时调用sdf.format(…)
, 那么可能会很混乱,因为sdf使用的内部数据结构可能会被并发的访问所破坏。
当然可以使用线程同步,但是开销很大,或者也可以在需要时构造一个局部SImpleDateFormat对象,但这很浪费,希望为每一个线程构造一个对象,即使该线程调用多次方法,也只需要构造一次,不必在局部每次都构造。
public static final ThreadLocal<SimpleDateFormat> sdf = new ThreadLocal<SimpleDateFormat>() {
@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd");
}
};
实现原理:
ThreadLocal的get方法就是从当前线程的ThreadLocalMap中取出当前线程对应的变量的副本。
该Map的key是ThreadLocal对象,value是当前线程对应的变量。
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
- 【注意,变量是保存在线程中的,而不是保存在ThreadLocal变量中】*
当前线程中,有一个变量引用名字是threadLocals,
这个引用是在ThreadLocal类中createmap函数内初始化的。
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
每个线程都有一个这样的名为threadLocals 的ThreadLocalMap
,以ThreadLocal和ThreadLocal对象声明的变量类型作为key和value。
Thread
ThreadLocal.ThreadLocalMap threadLocals = null;
这样,我们所使用的ThreadLocal变量的实际数据,通过get方法取值的时候,就是通过取出Thread中threadLocals引用的map
然后从这个map中根据当前threadLocal作为参数,取出数据。
每个线程内部都会维护一个类似 HashMap 的对象,称为 ThreadLocalMap
里边会包含若干了 Entry(K-V 键值对),相应的线程被称为这些 Entry 的属主线程;
Entry 的作用即是:
为其属主线程建立起一个 ThreadLocal 实例与一个线程特有对象之间的对应关系;
Entry 的 Key 是一个 ThreadLocal 实例,Value 是一个线程特有对象。
Entry 对 Key 的引用是弱引用;Entry 对 Value 的引用是强引用。
-
为什么ThreadLocalMap的Key是弱引用
- 如果是强引用,ThreadLocal将无法被释放内存。
- 因为如果这里使用普通的key-value形式来定义存储结构,实质上就会造成节点的生命周期与线程强绑定,只要线程没有销毁,那么节点在GC分析中一直处于可达状态,没办法被回收,而程序本身也无法判断是否可以清理节点。弱引用是Java中四档引用的第三档,比软引用更加弱一些,如果一个对象没有强引用链可达,那么一般活不过下一次GC。
当某个ThreadLocal已经没有强引用可达,则随着它被垃圾回收,在ThreadLocalMap里对应的Entry的键值会失效,这为ThreadLocalMap本身的垃圾清理提供了便利。
-
ThreadLocalMap是何时初始化的(setInitialValue)
在get时最后一行调用了setInitialValue,
它又调用了我们自己重写的initialValue方法获得要线程局部变量对象。
ThreadLocalMap没有被初始化的话,便初始化,并设置firstKey和firstValue;
如果已经被初始化,那么将key和value放入map。
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
- ThreadLocalMap 原理
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
它也是一个类似HashMap的数据结构,但是并没实现Map接口。
也是初始化一个大小16的Entry数组,Entry对象用来保存每一个key-value键值对,只不过这里的key永远都是ThreadLocal对象,通过ThreadLocal对象的set方法,结果把ThreadLocal对象自己当做key,放进了ThreadLoalMap中。
ThreadLoalMap的Entry是继承WeakReference,和HashMap很大的区别是,Entry中没有next字段,所以就不存在链表的情况了。
- 构造方法
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
- // 表的大小始终为2的幂次
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
- // 设定扩容阈值
setThreshold(INITIAL_CAPACITY);
}
- 在ThreadLocalMap中,形如key.threadLocalHashCode & (table.length - 1)(其中key为一个ThreadLocal实例)这样的代码片段实质上就是在求一个ThreadLocal实例的哈希值,只是在源码实现中没有将其抽为一个公用函数。
- 对于& (INITIAL_CAPACITY - 1),相对于2的幂作为模数取模,可以用&(2^n-1)来替代%2^n,位运算比取模效率高很多。至于为什么,因为对2^n取模,只要不是低n位对结果的贡献显然都是0,会影响结果的只能是低n位。
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
- getEntry(由ThreadLocal#get调用)
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
- // 因为用的是线性探测,所以往后找还是有可能能够找到目标Entry的。
return getEntryAfterMiss(key, i, e);
}
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
while (e != null) {
ThreadLocal<?> k = e.get();
if (k == key)
return e;
if (k == null)
- // 该entry对应的ThreadLocal已经被回收,调用expungeStaleEntry来清理无效的entry
- expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
- i是位置
- 从staleSlot开始遍历,将无效key(弱引用指向对象被回收)清理,即对应entry中的value置为null,将指向这个entry的table[i]置为null,直到扫到空entry。
- 另外,在过程中还会对非空的entry作rehash。
- 可以说这个函数的作用就是从staleSlot开始清理连续段中的slot(断开强引用,rehash slot等)
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// expunge entry at staleSlot
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
// Rehash until we encounter null
Entry e;
int i;
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
- // 对于还没有被回收的情况,需要做一次rehash。
如果对应的ThreadLocal的ID对len取模出来的索引h不为当前位置i,
则从h向后线性探测到第一个空的slot,把当前的entry给挪过去。
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null;
// Unlike Knuth 6.4 Algorithm R, we must scan until
// null because multiple entries could have been stale.
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}
#### set(线性探测法解决hash冲突)
private void set(ThreadLocal<?> key, Object value) {
// We don't use a fast path as with get() because it is at
// least as common to use set() to create new entries as
// it is to replace existing ones, in which case, a fast
// path would fail more often than not.
Entry[] tab = table;
int len = tab.length;
// 计算key的hash值
- int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
if (k == key) {
- // 同一个ThreadLocal赋了新值,则替换原值为新值
e.value = value;
return;
}
if (k == null) {
- // 该位置的TheadLocal已经被回收,那么会清理slot并在此位置放入当前key和value(stale:陈旧的)
replaceStaleEntry(key, value, i);
return;
}
}
// 下一个位置为空,那么就放到该位置上
tab[i] = new Entry(key, value);
int sz = ++size;
- // 启发式地清理一些slot,并判断是否是否需要扩容
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
- 每个ThreadLocal对象都有一个hash值 threadLocalHashCode,每初始化一个ThreadLocal对象,hash值就增加一个固定的大小 0x61c88647。
private final int threadLocalHashCode = nextHashCode();
private static final int HASH_INCREMENT = 0x61c88647;
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
- 由于ThreadLocalMap使用线性探测法来解决散列冲突,所以实际上Entry[]数组在程序逻辑上是作为一个环形存在的。
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
- 在插入过程中,根据ThreadLocal对象的hash值,定位到table中的位置i,过程如下:
- 1、如果当前位置是空的,那么正好,就初始化一个Entry对象放在位置i上;
- 2、不巧,位置i已经有Entry对象了,如果这个Entry对象的key正好是即将设置的key,那么重新设置Entry中的value;
- 3、很不巧,位置i的Entry对象,和即将设置的key没关系,那么只能找下一个空位置;
- 这样的话,在get的时候,也会根据ThreadLocal对象的hash值,定位到table中的位置,然后判断该位置Entry对象中的key是否和get的key一致,如果不一致,就判断下一个位置
- 可以发现,set和get如果冲突严重的话,效率很低,因为ThreadLoalMap是Thread的一个属性,所以即使在自己的代码中控制了设置的元素个数,但还是不能控制其它代码的行为。
#### cleanSomeSlots(启发式地清理slot)
- i是当前位置,n是元素个数
- i对应entry是非无效(指向的ThreadLocal没被回收,或者entry本身为空)
- n是用于控制控制扫描次数的
- 正常情况下如果log n次扫描没有发现无效slot,函数就结束了
- 但是如果发现了无效的slot,将n置为table的长度len,做一次连续段的清理
- 再从下一个空的slot开始继续扫描
- 这个函数有两处地方会被调用,一处是插入的时候可能会被调用,另外个是在替换无效slot的时候可能会被调用,
- 区别是前者传入的n为元素个数,后者为table的容量
private boolean cleanSomeSlots(int i, int n) {
boolean removed = false;
Entry[] tab = table;
int len = tab.length;
do {
i = nextIndex(i, len);
Entry e = tab[i];
if (e != null && e.get() == null) {
n = len;
removed = true;
i = expungeStaleEntry(i);
}
} while ( (n >>>= 1) != 0);
return removed;
}
#### rehash
- 先全量清理,如果清理后现有元素个数超过负载,那么扩容
private void rehash() {
- // 进行一次全量清理
expungeStaleEntries();
// Use lower threshold for doubling to avoid hysteresis
if (size >= threshold - threshold / 4)
resize();
}
- 全量清理
private void expungeStaleEntries() {
Entry[] tab = table;
int len = tab.length;
for (int j = 0; j < len; j++) {
Entry e = tab[j];
if (e != null && e.get() == null)
expungeStaleEntry(j);
}
}
- 扩容,因为需要保证table的容量len为2的幂,所以扩容即扩大2倍
private void resize() {
Entry[] oldTab = table;
int oldLen = oldTab.length;
int newLen = oldLen * 2;
Entry[] newTab = new Entry[newLen];
int count = 0;
for (int j = 0; j < oldLen; ++j) {
Entry e = oldTab[j];
if (e != null) {
ThreadLocal<?> k = e.get();
if (k == null) {
e.value = null; // Help the GC
} else {
int h = k.threadLocalHashCode & (newLen - 1);
while (newTab[h] != null)
h = nextIndex(h, newLen);
newTab[h] = e;
count++;
}
}
}
setThreshold(newLen);
size = count;
table = newTab;
}
#### remove
private void remove(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
if (e.get() == key) {
// 显式断开弱引用
e.clear();
// 进行段清理
expungeStaleEntry(i);
return;
}
}
}
Reference#clear
public void clear() {
this.referent = null;
}
内存泄露
-
只有调用TheadLocal的remove或者get、set时才会采取措施去清理被回收的ThreadLocal对应的value(但也未必会清理所有的需要被回收的value)。假如一个局部的ThreadLocal不再需要,如果没有去调用remove方法清除,那么有可能会发生内存泄露。
-
既然已经发现有内存泄露的隐患,自然有应对的策略,在调用ThreadLocal的get()、set()可能会清除ThreadLocalMap中key为null的Entry对象,这样对应的value就没有GC Roots可达了,下次GC的时候就可以被回收,当然如果调用remove方法,肯定会删除对应的Entry对象。
-
如果使用ThreadLocal的set方法之后,没有显式的调用remove方法,就有可能发生内存泄露,所以养成良好的编程习惯十分重要,使用完ThreadLocal之后,记得调用remove方法。
JDK建议将ThreadLocal变量定义成private static的,这样的话ThreadLocal的生命周期就更长,由于一直存在ThreadLocal的强引用,所以ThreadLocal也就不会被回收,也就能保证任何时候都能根据ThreadLocal的弱引用访问到Entry的value值,然后remove它,防止内存泄露。
Iterator / ListIterator / Iterable
- 普通for循环时不能删除元素,否则会抛出异常;Iterator可以
public interface Collection<E> extends Iterable<E> {}
- Collection接口继承了Iterable,Iterable接口定义了iterator抽象方法和forEach default方法。所以ArrayList、LinkedList都可以使用迭代器和forEach,包括增强for循环(编译时转为迭代器)。
public interface Iterable<T> {
Iterator<T> iterator();
default void forEach(Consumer<? super T> action) {
Objects.requireNonNull(action);
for (T t : this) {
action.accept(t);
}
}
- default Spliterator<T> spliterator() {
return Spliterators.spliteratorUnknownSize(iterator(), 0);
}
- }
- 注意这些具体的容器类返回的迭代器对象是各不相同的,主要是因为不同的容器遍历方式不同,但是这些迭代器对象都实现Iterator接口,都可以使用一个Iterator对象来统一指向这些不同的子类对象。
- ArrayList#iterator
public Iterator<E> iterator() {
return new Itr();
}
- ArrayList#Itr
private class Itr implements Iterator<E> {
int cursor; // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such
int expectedModCount = modCount;
public boolean hasNext() {
return cursor != size;
}
@SuppressWarnings("unchecked")
public E next() {
checkForComodification();
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
@Override
@SuppressWarnings("unchecked")
public void forEachRemaining(Consumer<? super E> consumer) {
Objects.requireNonNull(consumer);
final int size = ArrayList.this.size;
int i = cursor;
if (i >= size) {
return;
}
final Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length) {
throw new ConcurrentModificationException();
}
while (i != size && modCount == expectedModCount) {
consumer.accept((E) elementData[i++]);
}
// update once at end of iteration to reduce heap write traffic
cursor = i;
lastRet = i - 1;
checkForComodification();
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
- ArrayList#listIterator
public ListIterator<E> listIterator() {
return new ListItr(0);
}
- ArrayList#ListItr
private class ListItr extends Itr implements ListIterator<E> {
ListItr(int index) {
super();
cursor = index;
}
public boolean hasPrevious() {
return cursor != 0;
}
public int nextIndex() {
return cursor;
}
public int previousIndex() {
return cursor - 1;
}
@SuppressWarnings("unchecked")
public E previous() {
checkForComodification();
int i = cursor - 1;
if (i < 0)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i;
return (E) elementData[lastRet = i];
}
public void set(E e) {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
ArrayList.this.set(lastRet, e);
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
public void add(E e) {
checkForComodification();
try {
int i = cursor;
ArrayList.this.add(i, e);
cursor = i + 1;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
}
for /增强for/ forEach
For-each loop Equivalent for loop
for (type var : arr) {
body-of-loop
} for (int i = 0; i < arr.length; i++) {
type var = arr[i];
body-of-loop
}
for (type var : coll) {
body-of-loop
} for (Iterator iter = coll.iterator(); iter.hasNext(); ) {
type var = iter.next();
body-of-loop
}
-
增强for循环在编译时被修改为for循环:数组会被修改为下标式的循环;集合会被修改为Iterator循环。
-
增强for循环不适合以下情况:(过滤、转换、平行迭代)
-
对collection或数组中的元素不能做赋值操作;
-
只能正向遍历,不能反向遍历;
-
遍历过程中,collection或数组中同时只有一个元素可见,即只有“当前遍历到的元素”可见,而前一个或后一个元素是不可见的;
-
forEach
-
ArrayList#forEach继承自
-
Iterable接口的default方法
-
default void forEach(Consumer<? super T> action) { Objects.requireNonNull(action); for (T t : this) { action.accept(t); }
}
Comparable与Comparator
-
基本数据类型包装类和String类均已实现了Comparable接口。
-
实现了Comparable接口的类的对象的列表或数组可以通过Collections.sort或Arrays.sort进行自动排序,默认为升序。
-
可以将 Comparator 传递给 sort 方法(如 Collections.sort 或 Arrays.sort),从而允许在排序顺序上实现精确控制。还可以使用 Comparator 来控制某些数据结构(如TreeSet,TreeMap)的顺序。
1.9 继承
子类继承父类所有的成员变量(即使是private变量,有所有权,但是没有使用权,不能访问父类的private的成员变量)。
子类中可以直接调用父类非private方法,也可以用super.父类方法的形式调用。
-
子类构造方法中如果没有显式使用super(父类构造方法参数)去构造父类对象的话(如果有必须是方法的第一行),编译器会在第一行添加super()。
-
子类的构造函数可否不使用super(父类构造方法参数)调用超类的构造方法?
-
可以不用显式的写出super,但前提是“父类中有多个构造方法,且有一个是显式写出的无参的构造方法”。
1.10 内部类
- 在另一个类的里面定义的类就是内部类
- 内部类是编译器现象,与虚拟机无关。
- 编译器会将内部类编译成用$分割外部类名和内部类名的常规类文件,而虚拟机对此一无所知。
内部类可以是static的,也可用public,default,protected和private修饰。(而外部类即类名和文件名相同的只能使用public和default)。
优点
-
每个内部类都能独立地继承一个(接口的)实现,所以无论外部类是否已经继承了某个(接口的)实现,对于内部类都没有影响。
-
接口只是解决了部分问题,而内部类使得多重继承的解决方案变得更加完整。
-
用内部类还能够为我们带来如下特性:
- 内部类可以有多个实例,每个实例都有自己的状态信息,并且与其他外部对象的信息相互独立。
- 在单个外部类中,可以让多个内部类实现不同的接口,或者继承不同的类。外部类想要多继承的类可以分别由内部类继承,并进行Override或者直接复用。然后外部类通过创建内部类的对象来使用该内部对象的方法和成员,从而达到复用的目的,这样外部内就具有多个父类的所有特征。
- 创建内部类对象的时刻并不依赖于外部类对象的创建。
- 内部类并没有令人迷惑的“is-a”关系,他就是一个独立的实体。
- 内部类提供了更好的封装,除了该外部类,其他类都不能访问
-
只有静态内部类可以同时拥有静态成员和非静态成员,其他内部类只有拥有非静态成员。
成员内部类:就像外部类的一个成员变量
- 注意内部类的对象总有一个外部类的引用
- 当创建内部类对象时,会自动将外部类的this引用传递给当前的内部类的构造方法。
静态内部类:就像外部类的一个静态成员变量
public class OuterClass {
private static class StaticInnerClass {
int id;
static int increment = 1;
}
}
//调用方式:
//外部类.内部类 instanceName = new 外部类.内部类();
局部内部类:定义在一个方法或者一个块作用域里面的类
-
想创建一个类来辅助我们的解决方案,又不希望这个类是公共可用的,所以就产生了局部内部类,局部内部类和成员内部类一样被编译,只是它的作用域发生了改变,它只能在该方法和属性中被使用,出了该方法和属性就会失效。
-
JDK1.8之前不能访问非final的局部变量!
-
生命周期不一致:
-
方法在栈中,对象在堆中;方法执行完,对象并没有死亡
-
如果可以使用方法的局部变量,如果方法执行完毕,就会访问一个不存在的内存区域。
-
而final是常量,就可以将该常量的值复制一份,即使不存在也不影响。
public Destination destination(String str) {
class PDestination implements Destination {
private String label;
private PDestination(String whereTo) {
label = whereTo;
}
public String readLabel() {
return label;
}
}
return new PDestination(str);
}
匿名内部类:必须继承一个父类或实现一个接口
- 匿名内部类和局部内部类在JDK1.8 之前都不能访问一个非final的局部变量,只能访问final的局部变量,原因是生命周期不同,可能栈中的局部变量已经被销毁,而堆中的对象仍存活,此时会访问一个不存在的内存区域。假如是final的变量,那么编译时会将其拷贝一份,延长其生命周期。
- 拷贝引用,为了避免引用值发生改变,例如被外部类的方法修改等,而导致内部类得到的值不一致,于是用final来让该引用不可改变。
- 但在JDK1.8之后可以访问一个非final的局部变量了,前提是非final的局部变量没有修改,表现得和final变量一样才可以!
interface AnonymousInner {
int add();
}
public class AnonymousOuter {
public AnonymousInner getAnonymousInner(){
int x = 100;
return new AnonymousInner() {
int y = 100;
@Override
public int add() {
return x + y;
}
};
}
}
1.11 关键字
final
final在Java中是一个保留的关键字,可以声明成员变量、方法、类以及本地变量(代码块或方法中变量)。
一旦你将引用声明作final,你将不能改变这个引用了,编译器会检查代码,如果你试图将变量再次初始化的话,编译器会报编译错误。
- 修饰成员变量需要对其初始化并不可改变,如果修改则会报错
- 修饰方法代表是最终版本子类不能重写,同时比非final 方法快,因为在编译时候已经静态绑定了,不需要运行时动态绑定.
- 修饰类 不能被继承 如String或Integer等包装类
try-finally-return
- 不管有没有出现异常,finally块中代码都会执行;
- 当try和catch中有return时,finally仍然会执行;无论try里执行了return语句、break语句、还是continue语句,finally语句块还会继续执行;如果执行try和catch时JVM退出(比如System.exit(0)),那么finally不会被执行;
- finally是在return后面的表达式运算后执行的(此时并没有返回运算后的值,而是先把要返回的值保存起来,管finally中的代码怎么样,返回的值都不会改变,仍然是之前保存的值),所以函数返回值是在finally执行前确定的;
- 如果try语句里有return,那么代码的行为如下:
- 如果有返回值,就把返回值保存到局部变量中
- 执行jsr指令跳到finally语句里执行
- 执行完finally语句后,返回之前保存在局部变量表里的值
- 如果try语句里有return,那么代码的行为如下:
- 当try和finally里都有return时,会忽略try的return,而使用finally的return。
- 如果try块中抛出异常,执行finally块时又抛出异常,此时原始异常信息会丢失,只抛出在finally代码块中的异常。
实例一
public static int test() {
int x = 1;
try {
x++;
return x; // 2
} finally {
x++;
}
}
实例二:
private static int test2() {
try {
System.out.println("try...");
return 80;
} finally {
System.out.println("finally...");
return 100; // 100
}
}
static
- static方法就是没有this的方法。在static方法内部不能调用非静态方法,反过来是可以的。而且可以在没有创建任何对象的前提下,仅仅通过类本身来调用static方法。这实际上正是static方法的主要用途。
-
修饰成员方法:静态成员方法
- 在静态方法中不能访问类的非静态成员变量和非静态成员方法;
- 在非静态成员方法中是可以访问静态成员方法/变量的;
- 即使没有显式地声明为static,类的构造器实际上也是静态方法
-
修饰成员变量:静态成员变量
- 静态变量和非静态变量的区别是:静态变量被所有的对象所共享,在内存中只有一个副本,它当且仅当在类初次加载时会被初始化。而非静态变量是对象所拥有的,在创建对象的时候被初始化,存在多个副本,各个对象拥有的副本互不影响。
- 静态成员变量并发下不是线程安全的,并且对象是单例的情况下,非静态成员变量也不是线程安全的。
- 怎么保证变量的线程安全?
- 只有一个线程写,其他线程都是读的时候,加volatile,线程既读又写,可以考虑Atomic原子类和线程安全的集合类;或者考虑ThreadLocal
-
修饰代码块:静态代码块
- 用来构造静态代码块以优化程序性能。static块可以置于类中的任何地方,类中可以有多个static块。在类初次被加载的时候,会按照static块的顺序来执行每个static块,并且只会执行一次。
-
修饰内部类:静态内部类
- 成员内部类和静态内部类的区别:
- 前者只能拥有非静态成员;后者既可拥有静态成员,又可拥有非静态成员
- 前者持有外部类的的引用,可以访问外部类的静态成员和非静态成员;后者不持有外部类的引用,只能访问外部类的静态成员
- 前者不能脱离外部类而存在;后者可以
5.修饰import:静态导包
- 可以直接使用对应包中类的方法 如:
- 成员内部类和静态内部类的区别:
import static java.lang.Math.*;
// 可以直接使用random() 不需要加上Math.前缀
System.out.println(random());
switch
switch字符串实现原理
- 对比反编译之后的结果:
- 编译后switch还是基于整数,该整数来自于String的hashCode。
- 先比较字符串的hashCode,因为hashCode相同未必值相同,又再次检查了equals是否相同。
字节码实现原理(tableswitch / lookupswitch)
编译器会使用tableswitch和lookupswitch指令来生成switch语句的编译代码。当switch语句中的case分支的条件值比较稀疏时,tableswitch指令的空间使用率偏低。这种情况下将使用lookupswitch指令来替代。lookupswitch指令的索引表由int类型的键(来源于case语句块后面的数值)与对应的目标语句偏移量所构成。当lookupswitch指令执行时,switch语句的条件值将和索引表中的键进行比较,如果某个键和条件值相符,那么将转移到这个键对应的分支偏移量继续执行,如果没有键值符合,执行将在default分支执行。
abstract
- 只要含有抽象方法,这个类必须添加abstract关键字,定义为抽象类。
- 只要父类是抽象类,内含抽象方法,那么继承这个类的子类的相对应的方法必须重写。如果不重写,就需要把父类的声明抽象方法再写一遍,留给这个子类的子类去实现。同时将这个子类也定义为抽象类。
- 注意抽象类中可以有抽象方法,也可以有具体实现方法(当然也可以没有)。
- 抽象方法须加abstract关键字,而具体方法不可加
- 只要是抽象类,就不能存在这个类的对象(不可以new一个这个类的对象,必须是实现类才能new)。
this & super
-
this
-
自身引用;访问成员变量与方法;调用其他构造方法
-
- 通过this调用另一个构造方法,用法是this(参数列表),这个仅在类的构造方法中可以使用
-
- 函数参数或者函数中的局部变量和成员变量同名的情况下,成员变量被屏蔽,此时要访问成员变量则需要用“this.成员变量名”的方式来引用成员变量。
-
- 需要引用当前对象时候,直接用this(自身引用)
-
super
-
父类引用;访问父类成员变量与方法;调用父类构造方法
-
super可以理解为是指向自己超(父)类对象的一个指针,而这个超类指的是离自己最近的一个父类。
-
super有三种用法:
-
1.普通的直接引用,与this类似,super相当于是指向当前对象的父类,这样就可以用super.xxx来引用父类的成员,如果不冲突的话也可以不加super。
-
2.子类中的成员变量或方法与父类中的成员变量或方法同名时,为了区别,调用父类的成员必须要加super
-
3.调用父类的构造函数 必须在子类之前调用
访问权限
1.12 枚举
JDK实现
- 实例:
public enum Labels0 {
ENVIRONMENT("环保"), TRAFFIC("交通"), PHONE("手机");
private String name;
private Labels0(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
-
编译后生成的字节码反编译:
-
可以清晰地看到枚举被编译后其实就是一个类,该类被声明成 final,说明其不能被继承,同时它继承了 Enum 类。枚举里面的元素被声明成 static final ,另外生成一个静态代码块 static{},最后还会生成 values 和 valueOf 两个方法。下面以最简单的 Labels 为例,一个一个模块来看。
Enum 类
-
Enum 类是一个抽象类,主要有 name 和 ordinal 两个属性,分别用于表示枚举元素的名称和枚举元素的位置索引,而构造函数传入的两个变量刚好与之对应。
-
toString 方法直接返回 name。
-
equals 方法直接用 == 比较两个对象。
-
hashCode 方法调用的是父类的 hashCode 方法。
-
枚举不支持 clone、finalize 和 readObject 方法。
-
compareTo 方法可以看到就是比较 ordinal 的大小。
-
valueOf 方法,根据传入的字符串 name 来返回对应的枚举元素。
静态代码块的实现
- 在静态代码块中创建对象,对象是单例的!
- 可以看到静态代码块主要完成的工作就是先分别创建 Labels 对象,然后将“ENVIRONMENT”、“TRAFFIC”和“PHONE”字符串作为 name ,按照顺序分别分配位置索引0、1、2作为 ordinal,然后将其值设置给创建的三个 Labels 对象的 name 和 ordinal 属性,此外还会创建一个大小为3的 Labels 数组 ENUM$VALUES,将前面创建出来的 Labels 对象分别赋值给数组。
values的实现
- 可以看到它是一个静态方法,主要是使用了前面静态代码块中的 Labels 数组 ENUM$VALUES,调用 System.arraycopy 对其进行复制,然后返回该数组。所以通过 Labels.values()[2]就能获取到数组中索引为2的元素。
valueOf 方法
- 该方法同样是个静态方法,可以看到该方法的实现是间接调用了父类 Enum 类的 valueOf 方法,根据传入的字符串 name 来返回对应的枚举元素,比如可以通过 Labels.valueOf(“ENVIRONMENT”)获取 Labels.ENVIRONMENT。
- 枚举本质其实也是一个类,而且都会继承java.lang.Enum类,同时还会生成一个静态代码块 static{},并且还会生成 values 和 valueOf 两个方法。而上述的工作都需要由编译器来完成,然后我们就可以像使用我们熟悉的类那样去使用枚举了。
用enum代替int常量
-
将int枚举常量翻译成可打印的字符串,没有很便利的方法。
-
要遍历一个枚举组中的所有int 枚举常量,甚至获得int枚举组的大小。
-
使用枚举类型的values方法可以获得该枚举类型的数组
-
枚举类型没有可以访问的构造器,是真正的final;是实例受控的,它们是单例的泛型化;本质上是单元素的枚举;提供了编译时的类型安全。
-
单元素的枚举是实现单例的最佳方法!
-
可以在枚举类型中放入这段代码,可以实现String2Enum。
-
注意Operation是枚举类型名。
用实例域代替序数
- 这种实现不好,不推荐使用ordinal方法,推荐使用下面这种实现:
用EnumSet代替位域
- 位域是将几个常量合并到一个集合中,我们推荐用枚举代替常量,用EnumSet代替集合
- EnumSet.of(enum1,enum2) -> Set<枚举>
用EnumMap代替序数索引
-
将一个枚举类型的值与一个元素(或一组)对应起来,推荐使用EnumMap数据结构
-
如果是两个维度的变化,那么可以使用EnumMap<Enum1,Map<Enum1,元素>>
1.13 序列化
JDK序列化(Serizalizable)
-
定义:将实现了Serializable接口(标记型接口)的对象转换成一个字节数组,并可以将该字节数组转为原来的对象。
-
ObjectOutputStream 是专门用来输出对象的输出流;
-
ObjectOutputStream 将 Java 对象写入 OutputStream。可以使用 ObjectInputStream 读取(重构)对象。
serialVersionUID
-
Java的序列化机制是通过在运行时判断类的serialVersionUID来验证版本一致性的。在进行反序列化时,JVM会把传来的字节流中的serialVersionUID与本地相应实体(类)的serialVersionUID进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常。(InvalidCastException)。
- 如果没有添加serialVersionUID,进行了序列化,而在反序列化的时候,修改了类的结构(添加或删除成员变量,修改成员变量的命名),此时会报错。
- 如果添加serialVersionUID,进行了序列化,而在反序列化的时候,修改了类的结构(添加或删除成员变量,修改成员变量的命名),那么可能会恢复部分数据,或者恢复不了数据。
-
如果设置了serialVersionUID并且一致,那么可能会反序列化部分数据;如果没有设置,那么只要属性不同,那么无法反序列化。
其他序列化工具
- XML/JSON
- Thrift/Protobuf
对象深拷贝与浅拷贝
- 当拷贝一个变量时,原始引用和拷贝的引用指向同一个对象,改变一个引用所指向的对象会对另一个引用产生影响。
- 如果需要创建一个对象的浅拷贝,那么需要调用clone方法。
- Object 类本身不实现接口 Cloneable,直接调用clone会抛出异常。
如果要在自己定义类中调用clone方法,必须实现Cloneable接口(标记型接口),因为Object类中的clone方法为protected,所以需要自己重写clone方法,设置为public。
- protected native Object clone() throws CloneNotSupportedException;
public class Person implements Cloneable {
private int age;
private String name;
private Company company;
@Override
public Person clone() throws CloneNotSupportedException {
return (Person) super.clone();
}
- }
public class Company implements Cloneable{
private String name;
@Override
public Company clone() throws CloneNotSupportedException {
return (Company) super.clone();
}
}
- 使用super(即Object)的clone方法只能进行浅拷贝。
- 如果希望实现深拷贝,需要修改实现,比如修改为:
@Override
public Person clone() throws CloneNotSupportedException {
Person person = (Person) super.clone();
person.setCompany(company.clone()); // 一个新的Company
return person;
}
-
假如说Company中还有持有其他对象的引用,那么Company中也要像Person这样做。
-
可以说:想要深拷贝一个子类,那么它的所有父类都必须可以实现深拷贝。
-
另一种实现对象深拷贝的方式是序列化。
-
@Override
protected Object clone() {
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream os = new ObjectOutputStream(baos);
os.writeObject(this);
os.close();
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
ObjectInputStream in = new ObjectInputStream(bais);
Object ret = in.readObject();
in.close();
return ret;
}catch(Exception e) {
e.printStackTrace();
}
return null;
}
1.14 异常
Error、Exception
- Error是程序无法处理的错误,它是由JVM产生和抛出的,比如OutOfMemoryError、ThreadDeath等。这些异常发生时,Java虚拟机(JVM)一般会选择线程终止。
- Exception是程序本身可以处理的异常,这种异常分两大类运行时异常和非运行时异常。程序中应当尽可能去处理这些异常。
常见RuntimeException
- IllegalArgumentException - 方法的参数无效
- NullPointerException - 试图访问一空对象的变量、方法或空数组的元素
- ArrayIndexOutOfBoundsException - 数组越界访问
- ClassCastException - 类型转换异常
- NumberFormatException 继承IllegalArgumentException,字符串转换为数字时出现。比如int i= Integer.parseInt(“ab3”);
RuntimeException与非Runtime Exception
-
RuntimeException是运行时异常,也称为未检查异常;
-
非RuntimeException 也称为CheckedException 受检异常
-
前者可以不必进行try-catch,后者必须要进行try-catch或者throw。
异常包装
- 在catch子句中可以抛出一个异常,这样做的目的是改变异常的类型
try{
…
}catch(SQLException e){
throw new ServletException(e.getMessage());
}
这样的话ServletException会取代SQLException。
- 有一种更好的方法,可以保存原有异常的信息,将原始异常设置为新的异常的原因
try{
…
}catch(SQLException e){
Throwable se = new ServletException(e.getMessage());
se.initCause(e);
throw se;
}
当捕获到异常时,可以使用getCause方法来重新得到原始异常
Throwable e = se.getCause();
建议使用这种包装技术,可以抛出系统的高级异常(自己new的),又不会丢失原始异常的细节。
使用建议
- 早抛出,晚捕获。
- 在流程控制中能使用状态值返回的经量不要使用异常控制,比如登录业务中的密码错误,经量使用返回错误编码+错误提示,而不是抛出运行期异常:密码错误