第一章:Java程序设计概述
- Java和C++最大的不同在于Java采用的指针模型可以消除重写内存和损坏数据的可能性。
- Java中的int永远是32位,int的大小不低于short int,不高于long int;C/C++中int可能是16位也可能是32位,或是编译器提供商指定的其他大小。
第二章:Java程序设计环境
- JDK下载地址:http://www.oracle.com/technetwork/java/javase/downloads。
- Eclipse下载地址:http://eclipse.org/downloads。
- JDK要区分32位和64位;Java区分大小写。
- javac是一个编译程序,将.java文件编译成.class文件;java启动虚拟机,执行.class文件中的字节码文件。
第三章:Java基本程序设计结构
- Java是强类型语言,有8种基本数据类型:4种整型、2种浮点型、1种字符型、1种布尔型,分别为:byte、short、int、long、float、double、char、boolean。
- 占用字节数:byte:1字节、short:2字节、int:4字节、long:8字节、float:4字节、double:8字节;Java中所有数值类型所占字节数量与平台无关。
- byte与char区别:
• byte是有符号型,char是无符号型;byte占1字节,8位,表示-127到128,char占2字节,16位,表示0~65536。
• byte可以初始化为数字或字符,最终表示的是数值,char可以初始化为数字或字符,最终表示的是字符。
• char可以表示中文,byte不可以,对于英文可以相互转化,byte代表的是ASCII码,char代表对应的字符。 - 注意不要用浮点型来进行金融计算,如(2.0-1.1=0.89999...),因为二进制无法精确表示1/10;尽量不要在程序中使用char类型。
- 码点是指一个编码表中某个字符对应的代码值,在Unicode标准中采用16进制书写,并加上前缀U+;UTF-16编码采用不同长度表示码点,每个字符用16位表示,通常被称为代码单元,辅助字符一般采用一对连续的代码单元进行编码;最好少用char类型。
- 一个字符串与非字符串的值拼接时,后者会被转为字符串。
- 格式化输出:
("%1$d,%2$s",99,"abc")
->输出(99,abc)
;%是格式化关键字,$表示参数索引,索引必须在%后面,以$结尾,以1开头。 - &&的优先级高于||;+=是右结合运算符,a+=b+=c,会先进行b+=c,然后在+=a。
- C++中可以在嵌套语句块中定义重名变量,内层会覆盖外层变量,但是Java不可以。
- else语句与相邻最近的一个if语句为一对。
- case标签可以是char、byte、short、int、枚举常量和String(Java SE7后支持)。
- break:跳出循环体,签名定义好标签 break label可以跳出多层嵌套循环;continue:不执行后面的逻辑,直接进行下一次循环。
- 数组是一种数据结构,用来存储同一类型值的集合,创建数组时,数值型初始化为0、布尔型初始化为false、对象型初始化为null,一旦创建后就不能改变它的大小了。
- 创建数组三种方法:
•int[] a = new int[10];
•int[] a = {1,2,3};
•int[] a = new int[]{1,2,3}。
- 打印数组:
Arrays.toString(a)
;打印多维数组:Arrays.deepToString(a)
。 - 数组的
Arrays.copyOf()
方法拷贝一个新的引用;Arrays.sort()
方法使用了优化的快速排序算法来排序数组;Arrays.binarySearch()
采取二分查找算法查询指定值;还有fill()
、qeuals()
等方法。 - Arrays具有封装好的许多静态方法提供给我们对数组进行使用。如
toString()、copyOf()、copyOfRange()、sort()、binarySearch()、fill()、equals()
等,更多api见P85。
第四章:对象与类
- OOP对象的三个主要特征:行为(方法)、状态(方法的执行结果)、标识(多态)。
- 类之间常见的关系:依赖(uses-a)、聚合(has-a)、继承(is-a)。
- 一个对象变量并没有实际包含一个对象,而是引用一个对象;任何对象变量的值都是对存储在另一个地方的一个对象的引用,实际的对象在堆内存中;当一个对象包含另一个对象变量时,这个变量依然包含着指向另一个堆对象的指针。
- 所有的Java对象都是在堆中构造的,构造器伴随着new操作符一起使用,不能在构造器中定义与实例域重名的局部变量。
- 方法中的显示参数指传参,隐式参数指方法的主体类,即调用对象本身(this),如
data.getTime()
中的data。 - 对一个对象进行数据域的拷贝可以用Object的
clone()
方法,不要直接返回一个类的引用本身。 - final实例域在构造器执行之后一定是赋上值的且不可改变;static域属于类,不属于任何独立的对象。
- 类的成员方法可以访问静态变量,静态方法不能访问成员变量。
- 方法参数分为按值传递和按引用传递:值传递不可改变原值,因为先拷贝再处理;引用传递虽然也拷贝了一份,但是是浅拷贝,两个引用指向同一个内存地址,因此修改引用状态影响到双方。
- 方法总结(两种传递):
• 方法不能改变基本数据类型的参数(值传递,会拷贝)。
• 方法可以改变一个对象的状态(址传递,浅拷贝,改变了实际的堆里对象状态)。
• 方法不能让对象参数引用一个新的对象(会拷贝,只要不改变对象状态,只修改引用没用,因为方法结束时变量拷贝被丢弃,原引用和原对象是没变的)。 - 当且仅当类没有任何构造方法时,系统会为之创建一个无参构造方法,否则必须使用自己定义的构造方法。
- 初始化块会在构造类对象时被执行,先运行初始化块,然后才运行构造器主体部分。
- 类的加载顺序:静态变量、静态初始化块(先父后子) -> 实例变量、实例初始化块(先父后子) -> 构造器(先父后子)。
-
finalize()
方法会在垃圾回收器清除对象之前被调用;import导入包时,可以导入包、类和静态方法和静态变量。 - 几个概念区别:
• 重写(覆盖)(override)指方法名和参数完全相同;
• 重载(overload)指方法名相同参数不同;
• 多态(polymorphlism)是针对类来说的指父类的多种实现。 - 包权限优先级:
public(任何地方) > protected(本包和其他包子类) > default(本包) > private(本类)
。 - 只有public级别的方法可以在new出来的对象中去显式调用,否则都不行(只能在类中和子类中自己使用)。
重点:P112 静态域、静态方法 P123 对象构造
注意:
instanceof 关键字,运行时指出对象是否是一个特定类的实例(它是判断不出来接口、继承等关系中的子类和父类的,都会返回true),需要判断具体类时使用.class进行比较。
第五章:继承
- 反射:指在程序运行期间发现更多的类及其属性的能力,能够在运行时动态分析类、构造类、调用类的方法的行为,可使用Class类的
newInstance()
方法来动态构造类,Method的invoke()
来动态调用方法。 - super不是一个对象的引用,不能将其赋给另一个对象变量,它只是一个指示编译器调用超类方法的关键字,构造方法中super语句必须放到第一行。
- 若子类构造器没有显示调用父类的构造器,则系统自动调用父类的无参构造器;若父类没有无参构造器(有其他构造器),子类也没显示调用父类其他构造器,则编译会报错。
- 系统在运行时能自动选择调用哪个方法的现象称为动态绑定(重写的调用 系统知道引用类型是谁就行);编译时就能知道该调用那个方法称为静态绑定(如privite/static/final方法)。
- Java中只能单继承,但是继承链可以是有多层。
- 可以将一个子类实例赋值给超类变量(向下转型);不能将一个超类实例直接赋值给子类变量(需要强制类型转换)。
- 子类重写(覆盖)方法中,可见性不能低于超类,若是final方法将不能被重写(覆盖),不想它们在子类中改变语义。
- 包含一个或多个abstract方法的类必须被声明为抽象类;抽象类可以有自己的实例域和方法,子类必须重写父类的所有抽象方法,否则自己还是个抽象类;抽象类不能被实例化,只能实例化为具体的子类。
- Object类是所有类的超类,可以用它引用任何类型的变量,Java中只有基本数据类型不是对象。
-
equals()
方法默认判断的是内存地址,在子类定义equals方法时,首先调用超类的equals,如果失败直接返回false,然后再逐一对比子类自己的每个实例域,HashMap中如果重新定义equals()
方法就必须重新定义hashCode()
方法,保证hashCode相同则对象就相等,为了插入到散列表中的验证。 -
Objects.equals(a,b)
方法可避免两个变量都为null引起的问题;Arrays.equals()
方法可以检测两个数组是否相等(已重写判断每位是否相等的逻辑,类似String.equals())。 - 散列码(hash code)是由对象导出的一个整型值,是无规律的,每个对象的默认散列码是对象的存储地址,两个相等的对象要求返回相等的散列码(内存地址都一样了,指向同一个堆对象,所以可以用来判断对象相等)。
- ArrayList是一个泛型类,动态数组实现,动态查询时效率高,但是在中间插入和删除效率就不行了(后面的所有元素都要做位移),动态插入删除链表LinkedList效率高一些,只修改那个元素前后的指针即可。
- Java中所有基本数据类型都有一个与之对应的类称为包装器,一旦构造是不许更改的,在许多场景下可以自动装箱与拆箱,==判断的是地址,装箱和拆箱是编译器认可的,而不是虚拟机。
- Class类:
• 定义:在程序运行期间,Java运行时系统始终为所有对象维护着一个被称为运行时的类型标识,这个信息跟踪着每个对象所属的类,虚拟机利用运行时类型信息来选择相应的方法执行,独立于对象,存储着类的各种信息(构造器/域/方法)。
• 获取方式:
①Object.class
(只能获取到该类的class不区分父类子类引用)
②obj.getClass()
(当前obj的class区分父类子类)
③Class.forName(pkgClassName)
(动态获取class,可能不存在,反射一般用的多)
• 使用方法:Class的newInstance()
方法调用默认的构造器(无参构造器)动态创建一个类的实例,如果这个类没有默认构造器,则必须使用Constructor类中的newInstance()
中带参数的构造方法。
• 特点:一个Class对象实际上表示的是一个类型,而这个类型未必是一种类,例如:int不是类,int.class是一个Class类型的对象;Class类实际上是一个泛型类,虚拟机为每个类型管理一个Class对象,可用obj.getClass()==Object.class
来比较。 - 反射相关类:
• 在java.lang.reflect包中有三个常用类:Field、Method和Constructor分别用于描述类的域、方法和构造器,它们包含了一系列方法来分析类的各种属性和能力,外加一个Modifier类判断修饰符用,AccessibleObject三者公有的超类。
• 以上三个类有着公有的超类,以及各自的一些方法,getFields()
方法返回类及其父类的公有域;getDeclaredFields()
方法返回类的全部域,不包括父类。
• 如果f是一个Field类型的对象,obj是某个包含f域的类的对象,f.get(obj)将返回一个对象(Object类型,基本类型则返回包装器),其值为obj域的当前值(就类似拷贝一个域的对象而已)。
• f.set(obj,value)可以将obj对象的f域设置成新值,基本数据类型的域获取域使用如getDouble()
等方法返回包装器。 - 反射机制默认行为受限于Java的访问控制,可以调用
setAccessible()
方法来为反射对象设置可访问标志,setAccessible()
其实是AccessibleObject中的方法,它是Field、Method和Constructor的公有超类。 - ObjectAnalyzer将记录已经被访问过的对象,如:
new ObjectAnalyzer().toString(this)
。 - 反射常用方法:
• Class的getMethod(String name,Class<?>... parameterTypes)
方法返回一个method类(可以传入class...也可传入new Class[]{}数组);
• Method类中的invoke(Object o,Object... args)
方法调用包装在当前Method对象中的方法(可以传入obj...也可传入new Object[]{}数组),如果返回类型是基本类型,invoke方法返回其包装器类型;对于静态方法,可以把null
作为隐式参数传入;显示参数没有的话也传null。 - 程序中不要过多的使用反射,反射会影响性能,并且编译器很难发现程序错误,只有在运行时才会发现并导致异常。
- 反射创建数组:
• java.lang.reflect包中的Array类允许动态构建数组,Java数组会记住每个元素的类型(创建时指定),可以将一个特定数组临时转为Object[],然后可以强转回来,如果一开始就是个Object[],则不能强转。
• Array.newInstance(componentType,newLength)方法动态创建数组,getComponentType()
是Class的方法,getLength()
是Array的方法;创建得到的是Object数组,可以强转回来,Arrays.copyOf()
底层就是这么做的。
注意:
int[]可以转为Object,而不能转为Object[]。
- 重写、重载、覆盖、多态的区别:
• 重写(覆盖):方法级别的概念;@Override,出现在子类中,方法名和参数类型完全相同,子类重新实现父类定义的函数;但是访问权限不能小于父类的,抛出的的异常也不能大于父类;静态方法不能被重写为非静态方法。
• 重载:方法级别的概念;@Overload,出现在父类中,方法名相同,参数不同,类似多态的不同实现;不能通过访问权限、返回类型和抛出异常不同来重载。
• 多态:类级别的概念;父类的引用->子类的实例,使得父类可以调用子类中更多的方法;继承是子类使用父类的方法,而多态是父类调用子类的方法。
重点:P166 Object、equals方法 P190 反射
第六章:接口、lambda表达式与内部类
- 接口有三个主要作用:
①定义/描述统一的的业务方法规范,作为参数,传递实现类给别人调用;
②自己默认实现好逻辑交给他人备用,在该回调的时候触发回调;
③作为标签接口,标注某一种类型,如Serializable、Cloneable等。 - 接口中的所有实例域属于public static final的,所有方法属于public的,不能含有实例域;接口和抽象类的本质区别是接口是为了描述一组行为,抽象类是实现,可再作为父类完成一部分统一功能,可变部分交给子类去实现,接口只能定义规范,完全有实现类去实现。
- Java8中允许接口含有静态方法,default关键字提供默认方法,提供默认方法后实现类就可以不用全实现每个方法,可以复写该实现的方法。
- 方法冲突时两个原则:
①超类和接口的默认方法冲突时,始终超类优先;
②两个接口的默认方法冲突时,只要有其中任一个方法被默认实现了,子类就要选择性复写该方法(Person.super.getName()),会影响到每个接口。 - 对象的拷贝:
引用的赋值指创建一个栈内存引用,指向一个堆内存实际对象,然后另一个引用也指向这个堆内存,那么修改堆内存的对象对两个引用都是有影响的;拷贝是指完全克隆出一个独立的堆内存对象,这样修改就不影响两个引用了。
• 浅拷贝:对象的基本数据类型可以拷贝,但是引用类型没有被拷贝,还是同一个堆内存对象,两个引用会共享一些信息,修改时会影响到两个引用的状态。
• 深拷贝:对象的基本数据类型和引用数据类型都被拷贝,改变clone对象的值不会影响原引用。 - 对象的克隆:
• Object的clone()
方法是protected的,默认是浅拷贝,返回Object,如果对象想实现克隆,需要实现Cloneable接口和重写clone()方法,指定为public,且手动实现深拷贝,创建所有子引用类型并返回。
• Cloneable只是一个标记接口,没有任何方法,实现它只是为了标记一下该类具有克隆能力,使instanceof
可以通过,实现深拷贝的话该类和类中的引用类型都要实现Cloneable接口。
• 即使clone()
的默认浅拷贝可以满足需求,也得重写改为public才能调用,一般可以先super.clone()
拿到原父类返回的浅拷贝引用,再重定义对象里每个引用变量的值(实现深拷贝),并返回这个克隆对象。ps:使用Gson序列化反序列化也能实现克隆功能。 - lambda表达式:
• lambda表达式是一个可传递的代码块,可以在以后反复执行一次或多次,用于代替只有一个方法的接口;表现形式:参数,箭头(->)以及一个表达式
。(没有参数要提供空(),只有一个参数可不带(),表达式多行可用{},单行不用写return)
• lambda表达式中不能只在一个分支返回值而在其他分支不返回;lambda表达式中引用的外部自由变量的值必须是不可改变的;lambda表达式中this关键字指的是外围原对象this。
• lambda表达式表现的就是一个函数式接口,但Java是不能直接传递方法的引用的,其实还是封装了一层,内部应该也是传递对象的引用。 - 方法的引用:
"::"
操作符分割对象或类名与方法名,如Class::Method/object::Method
。构造器引用如Class::new
。 - 内部类:
• 定义:表示一种类之间的嵌套关系,而不是对象;解决命名控制和访问控制问题;内有内部类可以是私有的,外部类必须是包可见性或共有的。
• 构造方式:Outer.inner in = new Outer.inner()
或Outer.inner = outer.new Inner()
。
• 特点:可以访问自身数据域,也可访问外围数据域;内部类中所有静态域必须是final类型的,不能有static方法;内部类初始化时都会包含一个外部类的隐式引用,以便访问外部类的域和方法,因此说内部类会持有外部类的引用。
• 由于内部类会持有外部类的引用,因此在Activity中一般定义Handler的时候,如果进行耗时操作,handler中持有外部Activity,若Handler不会被销毁,这就可能会引起内存泄露(Act该销毁却没被销毁)。
• 特殊:局部内部类是定义在方法中的,不能有任何修饰符,对外部世界完全隐藏,访问的外部类实例域必须是final的(匿名内部类也是)。
• 匿名内部类多用于传递方法参数中的接口,无构造方法,需要在后面加{}内实现内部需要实现的方法;ArrayList的双括号初始化{{}}可构造出有初始化数据的List。
• 静态内部类不需要引用外围类对象,将内部类声明为static,便可取消对外部类产生的引用,所以如果内部类不需要访问外围类的实例域,最好声明为静态内部类;静态内部类可以有静态域和方法。 - 动态代理(proxy):
• 定义:可以在运行时创建一个实现了一组给定接口的新类,这种功能只有在编译时无法确定需要实现哪个接口时才有必要使用,代理类可以在运行时创建全新的类。
• 作用:代理的作用的是中间层,代理的是接口、方法,而不是对象,对于原本的对象和原方法的实现逻辑是可有可无的,代理的invocationHandler拿到方法后可以自己做逻辑处理,因此代理就是一个使用过程中的中间层替换作用。
• 应用场景:
①只有接口,没有实现类时(编译期还不知道要怎么实现)时,使用代理可以动态创建出这些接口的实现类,在每个接口方法被调用时,动态去实现相应的业务逻辑,如Retrofit中对Api类的解析。
②如果某个接口已经有实现类,并在工厂方法中返回了该实现类,如果需要对原本的实现逻辑前后加一些自己的业务逻辑,可以在工厂方法中Proxy.newInsrance()
创建代理类,实现自己新增的一些逻辑后将代理类返回,这时候可以把原来的类作为构造参数传入invocationHandler,原本的处理逻辑还是用原来的类去执行的。
③我们可以模仿Retrofit,定义业务接口,通过注解添加各种动态业务条件,然后在代理类invoke()
中分析方法并做各种动态处理,提高了业务逻辑的灵活性。
• 使用:Proxy.newInstance(Classloader loader,Class<?>[] interfaces,InvocationHandler handler)
;InvocationHandler中的invoke()
方法会统一所有接口方法的调用回调,在此能拿到method以及参数args,可以做自己对方法的实现逻辑,如果需要用到原对象和实现逻辑,可以把原对象也传进来。
• 特点:
①代理类一旦被创建就和普通类没啥区别了;代理类本身只有一个实例域——调用处理器,因此任何附加数据和原对象实例都要传到调用处理器中去使用。
②代理类一定是public和final,可以用isProxyClass()方法检测一个特定的Class对象是否代表一个代理类。
重点:P225 对象的克隆 P258 代理
注意:
代理与反射的区别是,反射构建对象时已经有该类的,只是动态去创建一个已存在类的实例;而代理是针对接口的,编译时还没该接口的具体实现类,因此动态去构建实现类和方法的实现逻辑,所有接口方法的分析都做好了放到invoke里的method里了,相当于在方法调用处加了统一的监听。
第七章:异常、断言和日志
- 所有的异常类都是由Throwable继承而来,下层分为Error和Exception,Exception又分为IOException和RuntimeException;只有IOException是受查异常(代码提示的异常),其余都是非受查异常(平时的崩溃,自己无法控制)。
- Error是Java形式内部错误和资源耗尽错误,我们无能为力,而程序中如果出现了RuntimeException,那一定是程序员自己的问题;对于异常要么throws抛出去给别人处理,要么try chahe自己处理;一个方法必须声明所有可能抛出的受查异常,而非受查异常是不可控的,应该避免。
- 子类覆盖超类的方法时,子类声明的受查异常不能比超类中声明的更通用;若超类中没有抛出任何受查异常,子类也不能抛出。
- 异常可以捕获也可以继续抛出,应该捕获那些知道应该如何处理的异常,而将那些不知道怎样处理的继续抛出(如工具类中的异常抛出,由调用者自己来分别处理)。
- catch语句中可以继续抛出异常,并将原始异常设置为新异常的原因
initCause()
,可以让用户抛出子系统中的高级异常,而不会丢失原异常的细节。 - try语句中可以只有finally而没有catch块,不管异常是否被捕获,finally语句块都会被执行,finally语句在方法返回前被执行,所以不要在finally中写return语句,它会覆盖掉原方法中的return。
- 堆栈轨迹(stack trace)是一个方法调用过程的列表,它包含了程序执行过程中方法的调用位置;
t.getStackTrace()
可以获取到一个StackTraceElementp[]数组,它包含了方法的name、className和调用行lineNumber等信息。 - 一般我们给
Thread.setDefaultUnchaughtExceptionHandler()
后,在程序非受查崩溃时会回调到Handler的uncaughtException(thread,throwable)
,在这里可以将throwable.printStackTrace(writer)
输出到文件里,然后将文件上传服务器帮助我们分析程序崩溃。 - 断言:
• 定义:断言机制允许在程序测试期间向代码中插入一些检查语句,当代码发布时,这些插入的检查语句会自动被移除。
• 表现形式:assert 条件
/assert 条件:表达式
。
• 开启断言:默认下断言是被禁用的,启用或禁用断言是类加载器(ClassLoader)的功能;启用或禁用使用java -ea/-da
,可针对整个项目或某个特定class或package作用域。
• 作用:断言相当于自己做了一层判断,如果符合预期则没什么,如果不符合预期会自动抛出一个AssertionError的非受查异常,不用在方法上自己去写throw了。
重点:264 异常分类、捕获异常
第八章:泛型程序设计
- 泛型程序设计指编写的代码可以被许多不同类型的对象所重用,使程序具有更好的可读性和安全性。
- 泛型变量指域和局部变量及方法的返回类型,泛型可以用于类中也可用于方法中,泛型方法中的类型变量放在修饰符后面,返回类型的前面。
- 泛型可以用
extends
关键字指定子类型来做限制,可以extends多个类型,用&
符间隔。 - 类型擦除:虚拟机中没有泛型类型对象,把泛型类加载到虚拟机中后,所有定义的泛型T都会被擦除成原始类型,有限定类型的话擦除类型变量替换为限定类型(extends多继承时取第一个类型,因此标签接口尽量往后放),无限定类型的话为Object。
- 类型擦除与多态的冲突:由于泛型擦除,一个泛型类指向其子类的引用,子类中如果对父类的方法重写(其实重写不了,父类参数是擦除后的Object,子类是具体的类型),在调用方法时,其实先调了父类的方法(Object类型),然后参数强转桥接到了子类的重写方法(具体类型),因此严格上说不能是重写,子类里是有两个方法的(可查看子类.class文件)。
- Java泛型转换总结:
• 虚拟机中无泛型,只有普通类和方法;
• 所有的参数类型都要用它们的限定类型替换;
• 桥方法被合成用来保持多态,为保持类型安全性,必要时插入强制类型转换。 - 泛型约束:
• 不能用类型参数代替基本类型;
• 类型检测只会检测原始类型;
• 不能实例化参数化类型数组(不安全);
• 不能实例化类型变量;
• 不能在静态域或方法中引用类型变量;
• 不能抛出和捕获泛型异常。 - 泛型之间无联系,不论
S
与T
有什么联系,Pair<S>
与Pair<T>
无任何联系;Class类本身是泛型,如String.class
是一个Class<String>
的实例。 - 通配符类型中,允许类型参数变化,有
<? super X>
与<? extends X>
,带有超类型限定的通配符可以向泛型对象写入,带有子类型限定的通配符可以从泛型对象读取。 - 泛型<T>与通配符<?>的区别:泛型T指定一种类型参数的表示,实例化时是什么就是什么;通配符?代表一种限制,只要符合条件类型是什么都行,可以有多种。
重点:P316 泛型擦除 P330通配符
第九章:集合与映射
- 概念:
• Java集合类库将接口与实现分离,接口定义增删改查等方法,具体分为集合和映射两种基本接口(Collection和Map);Collection接口实现了Iterable接口,往下又分为List、Set和Queue,Set往下有SortSet等;Map下有SortedMap等(都是接口);RandomAccess接口用于判断该集合是否支持高效的随机访问。
• 集合没有key值,映射有key和value;集合的实现可以使用具体的数据结构来存储数据,每种数据结构的特点不同,如Tree、优先级队列等是随机插入,按指定顺序输入,而Linked是按插入顺序输出。
• 常用的几种具体集合和映射:ArrayList、LinkedList、HashSet、TreeSet、LinkedHashSet、LinkedBlockingDeque等(Collection集合);HashMap、LinkedHashMap、WeadHashMap等(Map映射)。
• Java集合接口可以定义不同的实现,不同实现使用不同的数据结构,各种数据结构有自己的特点,另外结合泛型、迭代器,因此可以实现各种不同的数据存储方式;接口从Collection->List等有不同等级,每层接口定义的方法不同,逐步完善,中间也提供了一些抽象父类完成了一些基本实现,如元素默认判等使用equals()
方法,因此Set中我们可以自定义equals方法。 - 接口List可以添加重复元素,是有序的,Set不能添加重复元素,是无序的,所以元素被访问的顺序取决于集合类型。
- 迭代器:
• Java的迭代器位于两个元素之间,调用next()
时,迭代器越过下一个元素,并返回刚刚越过的元素,调用remove()
时将删除上次调用next越过的那个元素,调用remove之前没有调用next是不合法的,即不能连续两次调用remove。
•add()
方法插入元素到光标之前,add()
方法依赖于迭代器位置,remove()
方法依赖于迭代器状态;迭代器可以并发读,但要控制并发写;listIterator的set()
方法用一个新元素的值取代越过返回的那个元素,可返回局部迭代器。 - 集set中要适当的定义集的
equals()
方法,如只要两个集包含数据相同的元素(不要求顺序相同,不要求是同一个对象),就认为是相等的;相等的两个集合要求要有相同的散列码hashCode(注意子元素这一层定义equals()、hashCode()等方法,集合这一层的equals()里根据子元素再自定义判等和hashCode值)。 - List是有序集合,访问可以通过迭代器顺序访问或通过下标随机访问,但是不同数据结构随机访问性能差别很大,如随机插入、删除使用LinkedList(双向链表结构,数据保存在节点中,每个节点保存着前驱和后继的节点,便于插入删除;但是随机访问要挨个移动指针,因此不支持快速随机访问);随机查询使用ArrayList,可以通过索引来访问(动态数组结构,支持二分查找随机访问,中间增删要移动和赋值数据,效率低,需要把插入位置后所有元素往后移动)。因此ArrayList使索引访问更快,LinkedList最好使用迭代器遍历。数组动态查询之所以快是因为开辟连续的内存地址存储数据,找到一个位置就能知道其他位置,也因此是有界的,且动态插入效率低,而链表是将数据放在节点中,真正的数据内存地址不需连续,因此随机查询效率低,每次都要找出内存地址,但是随机插入高效,只需要改节点的指针即可,也满足了无界。
- LinkedList列表的
get()
方法实际上做了微小的优化,如果索引大于size()/2
的话就从列表尾端开始搜索元素,但是for循环中的list.get(i)
是效率很低的,每次都要从头遍历,因此随机查询不建议使用LinkedList;插入时add()
方法默认插入到尾部,插入中间可以用add(i)
或迭代器listIterable.add()
插入(依赖光标位置),listIterable迭代器也能返回局部迭代列表。 - Vector类和HashTable类的方法都是同步的,可以线程安全的访问对象,但是因此效率比较低,不考虑线程安全的话建议使用ArrayList和HashMap。
- 散列表:提供快速查找的数据结构,为每一个对象计算一个整数,称为散列码,散列码是由对象的实例域产生的一个整数;Java中散列表用链表数组实现,每个列表被称为桶;查找表中对象的位置,先计算散列码,与桶的总数取余,结果就是保存这个元素的桶的索引,因此只需要在这个桶中找数据即可。
- 桶中已有一个元素的情况称为“散列冲突”或“哈希冲突”,这时默认策略是按列表继续往后放元素(也有策略是寻找下一个桶),单个桶满时(有个默认桶满值如8)会从列表变为平衡二叉树(树结构查询更快);如果散列表太满(不论元素在哪个桶中,总的size/总桶数大于填充因子就算表满了),就需要再散列(以2的倍数扩桶),创建一个桶更多的表将所有元素插入到这个新表中,丢弃原来的表;装填因子决定何时对散列表再散列,默认为0.75。
- 所谓Set是无序的,因为算出的桶索引是无序的,add的元素不一定在哪个索引下;另外Set无重复元素因为
hashCode()
算出的桶索引基本不会一样,如果一样了,还会继续判equals()
是否一致,一致则覆盖,否则往后链表里加。 -
queals()
方法和hashCode()
的定义必须兼容,如果x.equals(y)
为true,x.hashCode()
也必须等于y.hashCode()
。 - 树集TreeSet是一个有序集合,按任意顺序插入,迭代器遍历时会顺序输出,排序使用红黑树实现;使用树集必须能够比较元素,因此元素必须实现Comparator接口或构造方法中传入一个Comparator;将一个元素插入到树中比插入到散列集要稍微慢一点点,但是检查数组或链表中的重复元素,树稍微快一点,所以单个桶满会将链表变二叉树。
- 队列接口可以在尾部添加元素,在头部删除元素,不可在中间插入,并且可以查找队列中元素的个数,队列接口的实现方式通常有循环数组(有界)和列表(无界)两种方式。
- 优先级队列的元素可以按任意方式插入,却总是按排序的顺序进行检索,内部使用堆(可自我调整的二叉树,类似红黑树,每次插入完就已经是有序的了)来存储数据,迭代并不是按照元素顺序访问,而删除却总是删除掉优先级最小的那个元素,也需要提供Comparator,多用于任务调度中任务队列的实现。
- 映射:不同于集合直接存放精确数据副本,而是用来存放键值对,通用分为HashMap和TreeMap;散列映射对键进行散列,树映射用键的整体顺序来排序。
get()
时没有对应键的信息,则返回null,键可以为null,值不能为null,重复调用put()
方法会覆盖掉上一次的值,put()
方法返回上一次的旧值(没有返回null)。 - 视图:集合框架认为映射本身不是一个集合,但它的视图是实现了Collection接口或某个子接口的对象。有三种:键集(Set)、值集合(Collection)、键/值对集(Set),KeySet并不是HashSet或TreeSet,只是实现了Set接口的某个类对象,既然是集合,就有迭代器,但是仅对原映射集可删除不能增加。
- WeakedHashMap弱散列映射解决映射强引用问题,当元素只被散列条目引用时,会将其加入到弱引用队列,等待垃圾回收机制将其回收;LinkedHashMap/Set可以记录插入顺序,但是使用访问顺序进行迭代(同一个桶中元素被访问后会从当前节点删除加入到链表尾部),且重写
removeEldestEntry()
方法可以实现类似LRU算法的Map,不过这种迭代顺序的影响只有发生散列冲突时才能看出来。 - Collections的静态方法
synchronizedMap()
方法可以将任意一个映射表转成线程安全的Map,不管是集合还是映射,是有并发问题的,不可一个迭代器正在修改,另一个迭代器在读,会抛出异常。 - Collections的
sort()
方法可以进行排序,shuffle()
方法可以进行乱序,binarySearch()
方法进行二分查找等,类似Arrays的一些方法。 - 属性映射Property是一种特殊的映射结构,键值都是字符串,可以
load(InputStream is)
加载,也可store(OutputStream out,String str)
写出。 - 栈Stack后进先出,有
push()
、pop()
、peek()
等方法,注意pop()
和peek()
如果站内无元素是会抛异常。 - 位集BitSet可高效存储位序列,元素包装在字节里,比ArrayList存Boolean效率高。
- 几种数据结构比较:
• 数组:采用一段连续的存储单元来存储数据。对于指定下标的查找,时间复杂度为O(1)
;通过给定值进行查找,需要遍历数组,逐一比对给定关键字和数组元素,时间复杂度为O(n)
,当然,对于有序数组,则可采用二分查找,差值查找,斐波那契查找等方式,可将查找复杂度提高为O(logn)
;对于一般的插入删除操作,涉及到数组元素的移动,其平均复杂度也为O(n)
。
• 链表:每个节点含有前驱和后继节点,因此对于链表的新增,删除等操作(在找到指定操作位置后),仅需处理节点间的引用修改即可,时间复杂度为O(1)
,而查找操作需要遍历链表逐一进行比对,复杂度为O(n)
,因为其没有下标的概念,只能通过执行几次next来找到目标元素。
• 二叉树:对一棵相对平衡的有序二叉树,对其进行插入,查找,删除等操作,平均复杂度均为O(logn)
。
• 哈希表:因为无序,数组链表结构,相比上述几种数据结构,在哈希表中进行添加,删除,查找等操作,性能十分之高,不考虑哈希冲突的情况下,仅需一次定位即可完成,时间复杂度为O(1)
。 - HashMap的实现原理:
内部采用散列表进行存储,散列表数据结构为数组+链表,数组保证快速定位,链表解决哈希冲突情况(策略有开放地址法、再散列函数法和链地址法等)。JDK1.7中默认容量16,链表变树临界值8,填充因子0.75(即threshold为12),size
表示当前Entry总数(不管是单桶还是多桶中),threshold
表示临界扩容桶值。在第一次put()
中才给table
赋值,hash()
对key值取hashCode进行一系列异或位移操作保证均匀分布,indexFor()
这里要求桶数每次都是2的幂,这样桶数-1后二进制一定是最高位0后面全1,不管是直接计算索引值还是再散列时对之前的所有元素重新计算index时,需要尽可能快,而hashCode不论多大与之进行&
运算时,最高位以前面都得0,后面&出来的结果就是余数大小,小小算法有点叼(位运算性能更高吧)。一般来说,对单桶,大于链表变树临界值则将链表变为树结构(树的查询更快),对多桶,总数大于threshold
临界值则需扩容,但是桶总数64之前,该链表变树时优先扩容,桶数大于64之后再该变树变树该扩容扩容,可能是优先尽量避免哈希冲突吧。
重点:都很重要
第十四章:并发
Thread:
static Thread currentThread() 获取当前线程
static void sleep(long millis) 休眠指定的毫秒数(当前线程)
static void yield() 导致当前线程处于让步状态(当前线程)
static boolean interrupted() 检测线程中断状态并置位为false(当前线程)
boolean isInterrupted() 只检测线程中断状态
void interrupt() 向线程发送请求,请求中断线程
void join() 等待终止指定的线程(其他线程调用)
Thread.State getState() 得到线程状态(6种)
void setPriority(int newPriority) 设置线程优先级 MIN_PRIORITY=1/NORM_PRIORITY=5/MAX_PRIORITY=10
void setDaemon(boolean isDaemon) 设置线程为守护线程,必须在线程启动之前调用
Object:
void wait() 导致线程进入等待状态直到它被通知
void nitify() 随机选择一个在该对象上调用wait方法的线程,解除其阻塞状态
vodi notifyAll() 解除那些在该对象上调用wait方法的线程的阻塞状态
- 多线程:
- 几个概念:
• 多任务系统:操作系统将CPU时间片分配给每一个进程,给人以并行处理的感觉,但是并发进程数并不是由CPU数目制约的。
• 多线程程序:一个程序(进程)可以同时执行多个任务,同时运行一个以上的线程。
• 进程与线程:每个进程拥有自己的一套变量,而每个线程共享数据;一个进程相当于一个应用程序,一个进程中可以包含多个线程,其中有一个主线程,其余都是工作线程,线程更轻量级。 - 不要直接调用Thread或Runnable的
run()
方法,它只会执行一个线程中的任务(当前线程),而不会启动一个新线程,要使用thread.start()
来开启(Runnable就是包装业务逻辑的类,在指定的时间和线程中执行某段代码)。 - 没有什么强制终止线程的方法,
interrupt()
用来请求终止线程;如果线程被阻塞(调用sleep或wait方法),interrupt()
将抛出异常;如果在中断状态被置位时调用sleep()
不会引起休眠且会清除中断状态并抛异常,因此中断置位后就别进行其他操作了;中断请求只是先标记一个状态,被中断的线程可以决定如何响应中断。 - 静态方法
interrupted()
检测当前线程是否中断且清除中断状态;实例方法isInterrupt()
检测是否被中断且不改变中断状态;实例方法interrupt()
请求中断。 - 线程的6种状态:New(新创建)、Runnable(可运行)、Blocked(被阻塞)、Waiting(等待)、Timed waiting(计时等待)、Terminated(被终止)。
- 调用
start()
后线程进入可执行状态,不一定是立即执行,取决于操作系统给线程提供运行的时间。这里安卓的Handler.post()
是等本方法体执行完,才有可能执行runnable里的逻辑,而线程的start()
方法后runnable里的执行逻辑和本方法体里的下一行不一定谁先执行呢。 - 两种操作系统调度方式:
• 抢占式调度:系统给每一个可运行线程分配时间片来执行任务,当时间片用完,操作系统剥夺该线程的运行权,并给另一个线程运行机会。
• 协作式调度:一个线程只有在调用yield方法、被阻塞或等待时,线程才会失去控制权。 - 几种状态的区别:
• 阻塞状态:一个线程试图获取一个对象的内部对象锁,而该锁被其他线程所持有,则该线程进入阻塞状态,当其他线程释放该锁,且线程调度器允许本线程持有它的时候,该线程变为非阻塞状态(正常竞争对象锁),相当于自动解锁(系统唤起),拿到了执行权。
• 等待状态:可理解为阻塞的一种,当线程已经获取到锁,发现自己条件不足无法执行,则自己主动释放锁,等待另一个线程通知调度器一个条件,它自己进入等待状态,如调用Object.wait()、thread.join()、Lock或Condition时出现(手动阻塞),这时必须被其他线程主动唤醒,否则可能出现死锁。
• 计时等待:等待状态一直保持到超时期满或者接收到适当的通知,则自己主动唤醒。如调用Thread.sleep()、Object.wait()、thread.join()、Lock.tryLock()或Condition.await()时出现。 - 每个线程有一个优先级,默认继承父线程优先级,范围为MIN_PRIORITY(1)~MAX_PRIORITY(10)之间,NORM_PRIORITY为5;当线程调度器有机会选择新线程时,它会优先选择优先级较高的线程,但是这个过程是高度依赖于操作系统的。
- 守护线程:调用
thread.setDaemon(true)
将线程转换为守护线程,唯一用途是为其他线程服务,当只剩下守护线程时,虚拟机就退出了;守护线程应该永远不去访问固有资源、文件等,因为它会在任何时候发生中断。 - 线程的
run()
方法不能抛出任何受查异常,但是,非受查异常会导致线程终止,这时候可以用setUncaughtExceptionHandler
(实例方法)来为任何一个线程安装一个处理器,也可以用Thread.setDefaultUncaughtExceptionHandler
(静态方法)来为所有线程安装一个默认的处理器;默认处理器默认为null,实例处理器默认为线程的ThreadGroup对象,ThreadGruop内部其实还是优先使用父处理器->默认处理器等来处理的,如果都没就System.err输出日志并崩溃退出。
- 同步:
- 同步的作用就是要保证一组操作的原子性。如线程安全队列保证了对该队列操作的线程安全(队列的内部锁),但是外部调用时如果有判断逻辑,就要保证判断逻辑上下的原子性(外部调用者的对象锁),最好每次使用都判下空,所以说其实也没有绝对的线程安全(锁的级别不同)。
- 锁对象Lock:
- Java提供了
synchronized
关键字和Java SE5中引入的ReentrantLock
类来解决线程同步问题,将某一段代码控制为原子性操作。 - 一旦一个线程封锁了锁对象,其他任何线程都无法通过lock语句,但其他线程调用
lock.lock()
时,它们被阻塞,直到第一个线程释放锁对象;把解锁操作放在finally
语句中很重要,如果代码抛出异常,锁必须被释放(注意异常退出的话要回滚避免对象损坏),如果使用锁,就不能使用带资源的try语句。 - 注意例子中每个对象有自己的
ReentrantLock
锁对象,如果两个线程试图访问同一个对象,那么锁以串行的方式提供服务,若访问不同的对象,则不会发生阻塞。 - 锁是可重入的,线程可以重复地获得已经持有的锁;锁保持一个持有计数器来跟踪lock方法的嵌套调用,线程在每一次调用
lock()
后都要调用unlock()
来释放锁;被一个锁保护的代码可以调用另一个使用相同的锁的方法,注意异常时要先回滚,否则对象可能处于一种受损状态。
- 条件对象Condition:
- 数据结构本身是安全的话(如例子中的Bank类的transfer方法),在外部调用如果有判断逻辑还是可能会不安全(如上面所讲锁对象已经不是一个层级的了),这时要么在外部使用加锁控制判断逻辑,要么在内部transfer方法添加条件锁判断逻辑。
- 条件对象:一个锁可以有一个或多个相关的条件对象。等待获得锁的线程和调用了await方法的线程本质上不同,一旦一个线程调用
await()
,它进入该条件的等待集,当锁可用时,该线程不是马上解除阻塞,而是等待直到另一线程调用同一条件上的signalAll()
方法为止(阻塞是得到锁自己被唤醒,等待是必须由其他线程来唤醒(相互await死锁现象,死锁发生的原因就是有超出条件限制的操作导致可能同时双方都不满足条件而相互等待))。 -
signalAll()
不会立即激活一个等待线程,它仅仅解除等待线程的阻塞,以便这些线程可以在当前线程退出同步方法之后,通过竞争实现对象的访问;signal()
注意是"随机"解除一个线程的阻塞,仅可在该条件上调用await()
、singal()
或singalAll()
,被激活的线程从之前await()
的方法出继续执行。
- Synchronized关键字:
- Java中的每一个对象都有一个内部锁,如果方法用
synchronized
声明,那么对象锁将保护整个方法。内部对象锁只有一个相关条件,wait()
方法添加一个线程到等待集中,notify()/nitifyAll()
方法解除等待线程的阻塞状态,这三个方法必须在synchronized
代码块中,也就是必须先拿到锁才能等待(condition也是先lock()后才能await()等)。缺陷:不能中断一个正在获得锁的线程、锁不能设置超时、条件是单一的。 - 实例方法上加
synchronized
关键字锁的是该实例的这个和所有同步实例方法,通过获取该对象的内部锁来实现;静态方法上加synchronized
关键字锁的是同一个类的这个或任何其它的同步静态方法,通过获取该类对象的内部锁实现。 - 实现同步的两种方式:
①在方法上添加synchronized
关键字来实现同步(使用自己的内部对象锁)。
②通过synchronized(obj)
进入一个同步阻塞。(使用一个其他Obj的对象锁,道理其实是一样的,都是拿到对象的内部锁来lock) - 注意条件对象有
await()、singal()、singalAll()
方法;对象是wait()、notify()、notifyAll()
方法。对象的几个方法内也是获取内部锁对象进行同步的,要放到synchronized
代码块中,先lock()
获取锁才能wait()
等操作。 - volatile关键字:
• 为实例域的同步访问提供一种免锁机制,更轻量级,如果声明一个域为volatile
,那么编译器和虚拟机就知道该域可能被另一个线程并发更新。
• 作用:禁止指令重排序(操作系统可以对指令集重排序的(分配内存->初始化对象->赋值空间地址给引用);实现可见性(针对变量);保证有序性(long、double等类型)。
• 注意:volatile保证可见性、有序性,不保证原子性,因为有序、可见是针对域的锁定,原子性是一系列操作,已经不是同一级别的锁了,可以在原子操作外部加锁,也可使用CAS(轻量级的判断/再赋值锁)。 - 多线程开发就是对多线程的阻塞与等待的控制,
synchronized
方法与lock()/unLock()
方法就是控制线程的阻塞(同步访问),在内部拿到锁以后可以执行了,发现自己条件不足,则自己再wait()/await()
让出锁,由其他线程进入同步执行,这时候线程自己就只能等待其他线程notify()/singal()
来唤醒了,否则可能导致死锁。
- 线程安全方法
- 有时候要避免共享变量,使用ThreadLocal<T>辅助类可以为各个线程构造单独的实例。ThreadLocal提供了
set()
和get()
访问器用来访问与当前线程相关联的线程局部变量;ThreadLocal中有个内部类ThreadLocalMap,key为ThreadLocal,value为变量T;每个线程有个变量threadLocals
(即ThreadLocalMap),在调用set()
方法时会取到线程当前这个map是否为空,空的话就createMap()
,否则就将key设为ThreadLocal,value设为原共享变量T的副本存进去,因此每个线程可以放多个ThreadLocal包装过的变量,这些变量线程间独享。 -
lock()
方法获取锁会阻塞;tryLock()
方法试图申请一个锁,在成功获得锁后返回true,否则立即返回false;lock()
方法不能被中断,如果出现死锁lock()
就无法终止,带超时的tryLock()
方法被中断会抛异常,允许程序打破死锁。 - 读写锁
ReentrantReadWriteLock
的readLock()
得到一个允许多线程读,排斥写的锁,或者writeLock()
得到一个排斥其他读写操作的锁。 - 线程的
stop()
方法会立即终止所有未结束的方法,包括run()
方法,因此有可能导致对象状态损坏,因此被弃用了;应当在合适的时候中断线程,线程会在安全的时候停止。
- 阻塞队列
- 对多线程问题,可以用一个或多个队列以安全的方式将其形式化,生产者向线程队列中插入元素,消费者从中取出它们;可以将例子中的转账指令插入某一队列,而另一个线程从队列中取指令执行,不直接操作bank对象,队列自己控制好线程安全即可,对bank对象进行操作(单线程执行,类似Handler,避免显示控制同步)。
- 当队列为空或者已满时,阻塞队列将导致操作的线程阻塞:
add()/remove()/element()
方法会抛异常、offer()/poll()/peek()
方法不阻塞也不会抛异常、take()/put()
方法会导致阻塞。 - 阻塞队列的几种类型:
• ArrayBlockingQueue(带有指定容量的循环数组阻塞队列)
• LinkedBlockingQueue(无上限链表阻塞队列)
• PriorityBlockingQueue(无边界的优先堆阻塞队列)
• DelayQueue(无边界的阻塞时间有限的阻塞队列) - 几种线程安全的集合:
• ConcurrenHashMap
• ConcurrentSkipListMap
• ConcurrentSkipListSet
• ConcurrentLinkedQueue
集合返回弱一致性的迭代器,不一定能反映出他们被构造之后的所有修改。 - 并发集视图:
ConcurrentHashMap.<T>newKeySet()
。
写数组拷贝:CopyOnWriteArrayList、CopyOnWriteArraySet,并发读,写的时候生成一个副本,占用双倍内存。 - 任何集合类都可以使用同步包装器变为线程安全的:
Collections.synchronizedList(new ArrayList<E>())
、Collections.synchronizedMap(new HashMap<K,V>())
。
- 线程池
- Runnable封装一个无返回值的异步任务,可用在线程或线程池中;Callable封装一个有返回值的异步任务,只能放在线程池中。
- Future保存并管理异步计算的结果;FutureTask实现了Ruannable和Future两个接口,可把自己提交给线程池,完了从自己拿处理结果。
- 如果程序中创建了大量生命周期很短的线程,或为了减少并发线程的数目,就应该使用线程池。
- 四种常用线程池:
• CachedThreadPool(核心=0,最大=MAX,0长度阻塞队列,空线程保持60s,适用于较多异步任务)
• FixedThreadPool(核心=最大=固定数,MAX长度阻塞队列,适用快速执行任务)
• SingleThreadPool(核心=固定=1,顺序执行任务)
• ScheduledThreadPool(延迟执行或延迟后周期执行的线程池)。 -
submit()
方法提交任务,得到一个Future对象用于管理结果;shutdown()
方法启动线程池关闭序列,被关闭的线程池不再接受新任务,所有任务执行完后线程池死亡;shotdownNow()
方法取消尚未开始的所有任务并试图中断正在执行的线程。
- 同步器
- 几种常用同步器:
• 信号量Semaphore:一个信号量管理许多的许可证,为了通过信号量,线程通过调用acquire()
方法请求许可,否则等待,信号量其实仅维护一个计数,因此用于控制同一时刻的线程数量,任何线程都可以通过release()
释放任意数目的许可。
• 倒计时门栓CountDownLatch:让一个线程集等待直到计数器变为0,用于暂停某个线程等待其他线程执行完必备操作后继续执行,它是一次性的,一旦计数器变为0,就不能再重用了。
• 循环栅栏CyclicBarrier:允许线程集到达一个公共栅栏(等待到指定数目)后执行某段逻辑,然后各自再继续执行(先到达的先await),如果任何一个在栅栏上等待的线程离开了栅栏,栅栏就被破坏了,如等待线程的await方法超时或者被中断了,这时其他线程的await方法会抛异常,那些已经等待的线程会立即终止await的调用。 - 同步队列:将生产者和消费者线程配对的机制,总是成对处理。当一个线程调用SynchronousQueue的
put()
方法时,它会阻塞直到另一个线程调用take()
方法为止,反之亦然;数据仅沿一个方向传递,从生产者到消费者,它不是一个队列,没有包含任何元素,size()
方法总是返回0。
重点:都很重要
《Java核心技术 • 卷Ⅰ》读书笔记,以个人角度理解记录,可作为复习摘要帮助快速拾起知识点,如有理解错误之处,欢迎指出。
补充知识点
-
Java内存分配:
①运行时数据区域:
方法区、堆区、栈区(虚拟机栈、本地方法栈)、程序计数器。
前两者是线程共享区、后面的是线程隔离区。
②各部分作用:
• 程序计数器:当前线程所执行的字节码的行号指示器,这个计数器记录的是在正在执行的虚拟机字节码指令的地址,当执行的是Native方法,这个计数器值为空;此内存区域是唯一一个没有规定任何OutOfMemoryError情况的区域 。
• Java虚拟机栈:线程私有的 ,它的生命周期与线程相同,存储方法执行时的局部变量表、操作数栈、方法出口等,含有基本数据类型、引用类型的空间地址,如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。
• 本地方法栈:与虚拟机的作用相似,不同之处在于虚拟机栈为虚拟机执行的Java方法服务,而本地方法栈则为虚拟机使用到的Native方法服务。有的虚拟机直接把本地方法栈和虚拟机栈合二为一。
• Java堆:所有线程共享的一块内存区域,在虚拟机启动时创建,此内存区域的唯一目的就是存放对象实例 ,是垃圾收集器管理的主要区域,由GC管理内存的回收,有一套自己的回收机制;如果堆中没有内存完成实例分配,并且堆也无法完成扩展时,将会抛出OutOfMemoryError异常。
• 方法区:又称静态存储区,各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据 ,运行时常量也是其中一部分;垃圾收集行为在这个区域比较少出现,但并非数据进了方法区就永久的存在了,这个区域的内存回收目标主要是针对常量池的回收和对类型的卸载;当方法区无法满足内存分配需要时,也将抛出OutOfMemoryError异常。
③对象的创建过程:
创建一个对象通常是需要new关键字,当虚拟机遇到一条new指令时,首先检查这个指令的参数是否在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过,如果没有那么执行相应的类加载过程;类加载检查通过后,虚拟机将为新生对象分配内存,为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来;Java程序通过栈上的reference数据来操作堆上的具体对象。主要的访问方式有使用句柄和直接指针两种:句柄就是引用中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息 ;直接指针指引用中存储的就是对象地址 。 -
ClassLoader
• ClassLoader的具体作用就是将class文件加载到jvm虚拟机中去,程序就可以正确运行了;jvm启动的时候,并不会一次性加载所有的class文件,而是根据需要去动态加载。
• class文件是字节码格式文件,java虚拟机并不能直接识别我们平常编写的.java源文件,所以需要javac这个命令转换成.class文件,所以其他语言编写的代码只要能编译成.class文件都可以被java虚拟机运行。
• 三种ClassLoader:
①BootstrapClassLoader:最顶层的加载类,主要加载核心类库,%JRE_HOME%\lib下的rt.jar、resources.jar、charsets.jar和class等。
②ExtClassLoader:扩展的类加载器,加载目录%JRE_HOME%\lib\ext目录下的jar包和class文件。
③AppClassLoader:也称为SystemAppClass 加载当前应用的classpath的所有类。
• 加载顺序:BootstrapClassLoader -> ExtClassLoader -> AppClassLoader。
• ExtClassLoader和AppClassLoader都是UrlClassLoader的子类。每个类加载器都有一个父加载器,AppClassLoader的父加载器是ExtClassLoader,ExtClassLoader的父加载器是null;一个ClassLoader创建时可以直接指定parent,类自定类加载器如果没有指定parent,那么它的parent默认就是Launcher.getClassLoader()
,即AppClassLoader。
• Bootstrap ClassLoader是由C/C++编写的,它本身是虚拟机的一部分,所以它并不是一个JAVA类,也就是无法在java代码中获取它的引用。
• 双亲委托机制:一个类加载器查找class和resource时,是通过“委托模式”进行的,它首先判断这个class是不是已经加载成功,如果没有的话它并不是自己进行查找,而是先通过父加载器,然后递归下去,直到Bootstrap ClassLoader,如果Bootstrap Classloader找到了,直接返回,如果没有找到,则一级一级返回,最后到达自身去查找这些对象,再找不到就抛异常了。虽然ExtClassLoader的parent为null,但是在找类时如果parent为null就调用findBootstrapClassOrNull(name)
从Bootstrap ClassLoader去找。
• 自定义ClassLoader可以根据自定义路径去加载class文件,从而将外部类加载进来并使用,一般复写findClass()
方法和在findClass()
方法中调用defineClass()
方法,可以根据自己的规定来进行类加密解密操作,并自定义类加载器来解析类。
网友评论