1 数据类型
1.1 章节概述
数据类型主要有基础数据类型、包装类型、String类型和大数类型。其中包装类(浮点类型除外)会带有缓存值,如果创建值在缓存范围内则会直接返回预先缓存的对象。String类型不属于基础类型,属于引用类型。String和包装类型都是不可变类。基础类型对象直接存的是值,而引用类型对象存储的是对象的引用。
基础类型和其对应的包装类:
- 整数类型:byte(Byte)、short(Short)、int(Integer)、long(Long);
- 浮点类型:float(Float)、double(Double);
- 字符类型:char(Character);
- 布尔类型:boolean(Boolean)。
大数类型:
- BigInteger:用来处理比long更大的整数,多大没有限制;
- BigDecimal:保证浮点型精度,主要解决二进制小数不精确的问题。
1.2 查漏补缺
为什么要有包装类?
让基础类型拥有对象(Object)的性质,比如可以加到容器中、默认值为null、可以进一步添加实用方法(如Character的isLowerCase())。
包装类两种创建方式的区别?
new Integer():始终新建一个Integer;Integer.valueOf(n):如果n在缓存范围内无论调用几次都是返回的同一个缓存对象Integer,超出缓存范围则新建一个。
为什么频繁拆/装箱影响效率?
对象(包装对象)在堆上,基本类型在栈上,装箱要去堆上申请空间(如果没有逃逸出方法,那也在栈上分配),拆箱要根据引用去找到包装类值并存入基本类型。对比对象间赋值(只改引用)和基础类型间赋值(只改值),装箱占用内存和CPU,拆箱占用CPU(多了一步根据引用获得值的操作)。
String类型是如何保证不可变的?
- 使用private final修饰存储String的值的数组,private保证无法通过继承和.value的方式修改,final保证数组的引用无法被修改。
- final修饰String类,使String无法被继承,防止继承后的误修改。
- 只要声明成final ,JVM才不用对相关方法在虚函数表中查询,而直接定位到string类的相关方法上,提高了执行效率。
- 构造函数会把传入的char[]或String复制一份作为自己的值,防止自己的值随外部传入值的改变而改变(因为传入的是数组,final只能保证引用不变,不能保证引用对象的值不变)。
不可变类型的好处?
并发更简单:不可变类型的变量不用担心在多线程共享该变量时由于并发控制不恰当而造成数据的读取错误或者修改失败等问题。因为这个值只允许读,并不会被修改,所以就可以省去对不可变类型的加锁。(具体实现可以通过final让变量不可被修改;或private修饰变量,并在方法中也不提供修改这些变量的功能,如果要外部要修改,内部新建一个对象返回)
降低使用容器时的出错概率:比如在使用HashSet/HashMap时,使用重写过equals方法(比如Person用姓名和年龄作为判断)的对象作为key,但这些值是可变的。此时,一个不清楚我们的equals规则的人新建了一个person加入到HashSet中,然后修改了person的年龄,按照他的理解大概率认为这个person和旧的是相等的,如果他基于这个前提判断person是否在hashSet中,理应得到true,但是因为我们重写了equals规则,且Person属性是可变的,最终判断的结果是false,造成了误解。
2 流程控制
2.1 章节概述
流程控制包含了指挥计算机干活的最基础的控制语句,其中包括分支(if…else…, switch…case…)、循环(while…, do…while…, for…)、跳转(break+lable(通过“lable:”声明位置), continue+lable)。
2.2 查漏补缺
switch对String的支持?
switch本来只支持整型的(char是ascii码),但在Java 7之后开始支持使用"String"类型作为判断条件。实际上,这只是一个语法糖,经过编译后的实际流程是:先获得String的hashcode,然后用hashcode作为进入某个分支的判断条件,进入分支后再调用equals方法判断是否是同一个对象,最终决定这个分支的内容是否执行。
for和for-each的区别?
使用for-each遍历数组时,其实是通过语法糖省去了普通for循环的写法,但实际上编译后还是普通的for循环;而如果遍历的是可迭代的类型,比如List,则是创建了一个迭代器,然后用.next()方法遍历。
for-each的坑?
增强for循环遍历集合时,如果使用集合自带的方法修改集合的长度,会发生ConcurrentModificationException异常。
List<Cat> cats = new ArrayList<>();
for(int i=0; i<10; i++){
cats.add(new Cat());
}
for (Cat cat : cats) {
cats.remove(cat); // cats.add(new Cat());也不行
}
原因分析:
首先应该知道这个异常会在什么时候触发,通过源码得知当出现modCount != expectedModCount时触发上述异常。
然后应该知道modCount和expectedModCount分别是什么,modCount记录了集合被修改的次数,当调用集合自带的修改集合的方法,modCount就会自增1;而使用for-each时会生成一个Iterator来遍历集合,在Iterator初始化时会执行int expectedModCount = modCount来保证两者相等。
因此异常出现的原因的就很显而易见了——集合自带的修改方法会改变modCount的值,导致异常被触发。
此外,通过以上分析,我们还可以预见如果使用了其他修改modCount的操作,如排序、元素替换等,都会触发异常。
解决:
使用Iterator中的方法修改元素,而不是集合自带的方法。
拓展:
为什么要设计成这样呢?这个设计主要是为了避免在并发编程中,其他线程在非预期的情况下修改了我正在遍历的集合。(即我希望的可能是我正在遍历的集合的值就是我开始遍历时的样子,不要有改动过)
3 类和对象
3.1 章节概述
类是对数据类型进一步整合,定义了某类实体的结构和动作,类是面向对象编程的一个核心部分,面向对象编程思想需要通过类来实践。
3.2 查漏补缺
面向对象的形象理解?
面向对象是人类思维的,面向过程是计算机思维的。
例:做鱼香肉丝
面向过程:自己买材料,肉,鱼香肉丝调料,蒜苔,胡萝卜等等然后切菜切肉,开炒,盛到盘子里。
面向对象:去饭店,张开嘴:老板!来一份鱼香肉丝!
如何指定类的子类?
Java 15起,用sealed修饰某个类,用permits指定可继承该类的子类名,例:public sealed class Shape permits Rect, Circle, Triangle{…}
对象的内存布局(对象头、实例数据、对齐填充)?
这里涉及到一个比较重要的点是,对象头和锁的状态的关系(以下数字都是数值,而不是所占位数):
- 无锁时:hashcode|age|biased_lock(偏向锁标记位):1|state(状态):01-----------对象被创建时的初始状态是hashcode、age都为0,最后三位为101(1表示可偏向,01表示无锁)
- 偏向锁:thread(线程号)|epoch(时间戳)|biased_lock:1|state:01------------------对象被创建后,经过3s延迟,对象头的默认状态
- 轻量级锁:ptr_to_lock_record(锁记录的引用)|state:00
- 重量级锁:ptr_to_heavyweight_monitor(monitor的引用)|state:10
- 待垃圾回收:state:11
4 异常
4.1 章节概述
异常同样是一个类,异常分为Error和Exception,都继承自Throwable类。Error表示严重的错误(OutOfMemoryError, StackOverflowError, etc.),一般无法通过程序自身解决的;Exception又分为unChecked Exception(无需捕获的异常,运行时异常)和Checked Exception(在编码阶段就要预先准备处理的异常,如IOException等)。Error和Exception都可以catch(catch的对象继承自Throwable类),但是Error一般无法自行处理,所以不建议catch。
5 反射
5.1 章节概述
反射让程序在运行时能够动态获取程序自身信息,修改自身结构和行为。具体来说,反射允许我们动态地加载并使用类,而不需要在编码时就确定,使得代码更通用。反射主要使用的类有Class、Field、Method、Constructor,一些基本的使用如下:
// 1.获取Class对象
Class clazz = obj.getClass();
Class clazz = String.class;
Class clazz = Class.forName("java.lang.String");
// 2.获取字段
Field field = clazz.getField("field name"); // 只能获取public修饰的字段
Field field = clazz.getDeclaredField("field name"); // 可获取private修饰的字段
Field[] fields = clazz.getFields(); // 获取所有public修饰的字段
Field[] fields = clazz.getDeclaredFields(); // 获取所有字段
//2.1 获取字段信息
String fName = field.getName(); // 返回字段名称,例如,"name";
Class fClass = field.getType(); // 返回字段类型,也是一个Class实例,例如,String.class;
int fModifier = field.getModifiers(); // 返回字段的修饰符,它是一个int,不同的bit表示不同的含义。
// 3.获取和设置字段值
field.setAccessible(true); // 将访问权限设置为开放,但如果修改Java核心库还是会被禁止
Object value = field.get(obj); // 获取对象obj的field字段的值
field.set(obj, aValue); // 将obj对象的field字段设置为aValue
// 4.获取方法
Method method = clazz.getMethod("methodName", paramTypes);
Method method = clazz.getDeclaredMethod("methodName", paramTypes);
Method[] methods = clazz.getMethods();
Method[] methods = clazz.getDeclaredMethods();
// 4.1 获取方法信息
String mName = method.getName(); // 返回方法名称,例如,"setName"
Class returnType = method.getReturnType(); // 返回方法的返回类型,也是一个Class实例,例如,void.class
Class[] paramTypes = method.getParameterTypes(); // 返回参数类型数组
int modifier = method.getModifiers(); // 返回方法的修饰符,它是一个int,不同的bit表示不同的含义。
// 4.2 调用方法
Object out = method.invoke(obj, inputs);
// 5.创建实例
Object instance = clazz.newInstance(); // 只能调用public且无参构造的方法
Constructor constructor = clazz.getConstructor(paramTypes...); // 获取参数类型为paramTypes的构造器,如果不填则是无参构造器
Object instance = constructor.newInstance(args...); // 实例化对象,args是参数
// 6.获取继承关系
Class parentClass = clazz.getSuperclass();
Class[] interfaces = clazz.getInterfaces(); // 只能获取当前类接口,不包括其父类
Boolean bool = clazz.isAssignableFrom(fromClass); // clazz是否能从fromClass转换过来(多态)
6 集合/容器
6.1 章节概述
集合(也叫容器)的主要功能是用来存储对象,集合基于两个根接口Collection和Map进行衍生。Collection用来存储单一元素,而Map用来存储键值对。
- Collection:List(按添加有序)、Queue、Set(无序,值唯一);
- Map:HashMap(key值通过equals和hashcode来保证唯一,具体来说hashcode找到桶位置,equals进一步比较),HashTable(线程安全),TreeMap(红黑树实现值的有序)
各实现类的数据结构:
- LinkedList:双向链表;
- ArrayList:数组+扩容;
- Vector:数组+扩容;
- HashMap:数组+链表/红黑树;
- LinkedHashMap:HashMap的节点间使用双向链表连接;
- HashSet:复用HashMap;
- TreeSet:红黑树;
- ArrayDeque:双向扩容数组;
- PriorityQueue:数组实现的堆;
6.2 查漏补缺
Collection、List和Set的add有什么区别?
"collection"接口的"add"方法不保证唯一性和有序性,"list"的"add"方法实现了存储的有序,而"set"的"add"方法可以确保元素的唯一性。
ListIterator双向移动?
ListIterator是List才有的,能够双向移动。
7 泛型
7.1 章节概述
泛型是JDK 5引入了新特性,主要功能一方面是能够在编码时就限定参数类型,而无需在运行时来进行强制类型转换(比如List list就在编码阶段限定了元素只能是Integer,而无需后续转换);
另一方是提高了代码的复用性,不同类型的数据使用同一个逻辑处理时,无需再额外编码,数据类型在参数传入时确定。由于泛型属于后加入的新特性,因此为了与旧版本ava兼容,引入了泛型擦除,使得代码编译后仍是与之前版本的Java相同。
7.2 查漏补缺
泛型擦除有什么影响?
- 泛型擦除的定义:泛型在运行时会被擦除转换为泛型的边界类型(为了与旧的Java代码兼容,旧代码没有泛型,所以运行时泛型要被转换),如<T>->Object,<T
extends Number>->Number,<? super Integer>->Object(注意,<T super
Integer>是不合法的) - 泛型无法直接实例化:即情况下,new T()是不行的,需要通过Class<T> clazz获取T的Class然后反射实例化;
- 编译时要求代码不能有歧义:即如果有方法method(List<? extends Number>
list),那么list.add(1)是不合法的,因为编译时并不知道“?”会变成Integer还是Double。
泛型数组?
第一种写法:
直接创建泛型数组是不允许的,因为运行时String会被擦除为Object,然后这个数组就能引用任意类型的变量的,也就是说new Pair<String,String>[2]不能保证数组的值就是我们想要的String,造成了歧义,所以编译不通过。
第二种写法:
表明我们知道这种风险,因此可以通过编译。
第三种写法:
new Pair[2]会被类型推断自动转换。
//编译错误,编译器无法保证开发者给出的类型,因此编译出错
Pair<String,String>[] ps = new Pair<String,String>[2];
//成功
Pair<String,String>[] ps = (Pair<String,String>[]) new Pair[2];
//成功
Pair<String,String>[] ps = new Pair[2];
List和List有什么区别(不指定泛型和指定用Object的区别)?
先说结论,指定泛型用Object那么可以这个变量可以为任意Object的子类,如果不指定那么就是这个泛型的边界类型。
在List和List的例子中,使用上确实没有区别,因为List没有指定边界,那么T对应的边界类型就是Object;但是如果是List<T extends Animal>那么如果不指定T的类型,T就会被转化成Animal类型。
泛型继承的多态性?
泛型的继承的多态性只能在类上,而不能在泛型上。比如List list = ArrayList<Integer>可以,但是ArrayList<Number> list = ArrayList<Integer>不行。编译器说,这我可不能给你保证哈(都会擦除为Object),所以就报错了。
为什么List<?> list = new ArrayList<Integer>()不能添加除null之外的元素?
这还是多态的问题,在编译时不知道list中指定的泛型是什么,自然不能添加元素,但可以删,因为删除不需要知道泛型。(顺口溜:编译看左边,运行看右边)
为什么List list = Arrays.asList(arr)返回的list不能改变元素数量?
这返回的是一个固定长度的List(其实就是原数组,改变原数组的值,这个也会改变),建议用new ArrayList<>(Arrays.asList(arr))。
]