【思维导图】java
面向对象三大特性
封装
封装就是使用private修饰属性或方法,这样类的对象就没法直接访问或修改属性,只能通过get/set方法进行访问或修改。
封装的好处:降低代码的耦合度,利于维护;通过get/set方法访问属性的时候,可以增加一些额外的逻辑,这是单独访问属性无法做到的。
继承
继承就是子类继承父类,子类继承了父类所有的方法和属性。并且子类还可以增加一些自己的属性和方法。
继承的好处是提高代码的复用性,比如说一个类想要拥有另一个类的属性和方法,就可以采用继承的方式实现。
多态
多态就是父类引用指向子类的实例,多态只有在运行时才能确定调用的是哪个类的方法。
多态的好处是提高代码的复用性,比如说使用一个方法时将参数设置为父类的引用,来接收各种各样的子类实例。这样写一个方法就可以有各种效果。
面向对象的理解
面向对象就是万事万物抽象为对象,将一些行为、特点抽象为方法和属性。
面向对象的好处就是使代码的耦合度降低,我们要实现什么功能时直接调用对象的方法即可。比如说要实现开门这个动作,面向对象的思路就是先抽象出door这个对象,然后再抽象出door的一些特征作为属性,比如说门的大小、颜色。然后抽象出door的开门、关门的方法。我们如果想要实现开门动作的话就直接调对象的这个方法即可。
重载与重写的区别
重载就是在同一个类中的多个方法,方法名相同,但是方法参数类型或参数个数不同。在编译期间就可以确定是调用哪个方法。
重写就是子类重写父类的方法。
重载算多态的体现吗
不算,重载是编译期间就确定要调用的方法,而多态是要在运行时才确定要调用的方法。
反射机制
反射就是获取类在运行时的大Class实例,只有获取了这个实例才能获取有关这个类的各种信息比如方法、属性。原理是因为jvm在加载类时都会为这个类在堆中生成大class实例,并且指向方法区中有关这个类的各种信息。
泛型
泛型就是规定一种类型。比如说我们创建一个list,我们就可以使用泛型规定list里的每个元素的参数类型。
另一方面,当方法的形参类型不确定的时候,也可以使用泛型,这样就可以提高方法的可复用性。方法放进去的是时候,取出来的就是什么。相较于多态,可以避免强制类型转换的异常。
序列化与反序列化
序列化就是将对象转换为字节流或者json文本格式的过程,反序列化就是由字节流或json文本格式转换为对象的过程。
对象只有序列化后才能进行传输和存储。
比如说当我们想要将对象存进redis中的value里,我们可以使用json序列化器,但是由于序列化为json后会多存储一个类路径,浪费空间。因此我们会选择String序列化器,首先将对象手动转变为json格式的字符串,将字符串转换为字节流传输。
String、StringBuilder、StringBuffer的区别
1、可变性
String是不可变的,因为String底层是一个private final修饰的字符数组。final意味这个字符数组的地址值不能改变,并不意味着字符数组里的内容不能改变,String真正不可变的原因在于这个private。由于是private修饰的,并且String没有提供修改这个字符数组的方法,因此没有任何渠道能修改这个字符数组,因此String是不可变的。
而StringBuilder和StringBuffer底层就只是字符数组,因此是可变的。
2、线程安全性
String是不可变的,因此String是线程安全的。
由于StringBuilder底层方法没有加锁,因此是线程不安全的,而StringBuffer底层方法是加了synchronized锁的,因此是线程安全的。
3、性能
对于字符串的拼接,如果String类型的变量使用“+”来拼接,底层会新创建一个String对象,而原来那个String对象就变成了无引用,使得堆中垃圾变多,gc时间变多,自然影响性能。而对于StringBuilder类型与StringBuffer类型使用append方法,不会新创建对象,效率更高,而StringBuilder性能又会比StringBuffer高,因为底层没有加锁。
hashCode() & equals()
hashCode方法就是获取对象的哈希码。而哈希码主要用于确定对象在哈希表中的下标位置。比如说就有用在HashMap、HashSet中。
hashCode方法在HashMap或HashSet中经常配合equals方法使用,因为hashCode存在哈希碰撞问题,所以不同对象的哈希码可能相同,那么就需要使用equals进一步判断对象是否相同。另外,如果没有hashCode方法只有equals方法时,就需要一个一个对象的进行比较,效率很低,所以hashCode的作用使得查找比较的效率提高。
clone()
浅拷贝与深拷贝:
子类直接调用父类(也就是Object类)的clone的方法,就是浅拷贝。浅拷贝会在堆上创建新的对象,但是如果对象内部的属性是引用类型的话,浅拷贝只会复制这个内部对象的引用地址,浅拷贝后的对象与原对象共享这个内部对象。
深拷贝就是需要重写clone方法,手动进行深拷贝。深拷贝就是不仅会在堆上创建新对象,也会创建新的内部对象。
wait() & notify()
共同点:这两个方法都可以让线程暂停执行。让线程变成等待态。
不同点:
1、sleep方法时间到了,线程会自动变成运行态;而wait方法需要等待其他线程调用同一个对象的notify方法后,线程才会变成运行态。
2、wait方法会使线程释放锁,而sleep方法不会使线程释放锁。
3、wait方法是在Object类里的方法,而sleep方法是在Thread类里的方法。
为什么wait方法是在object类的?
因为wait方法需要释放当前线程所占有的锁,又因为这个锁是对象锁,因此wait操作的应该是对象,而不是线程。
集合框架图
ArrayList
ArrayList & LinkedList 区别
ArrayList底层是一个数组,因此随机访问元素的速度很快,但是插入和删除的速度就很慢,时间复杂度是O(n)级别的。
LinkedList底层是一个双向链表,插入和删除速度快,但是随机访问元素的速度就很慢,需要依次遍历。
ArrayList初始化
如果在new ArrayList的时候没有指定集合容量,那么底层就会初始化一个数组大小为0的空数组,当第一次add元素时就会将数组的长度扩容到默认长度10的大小。
启发:当我们初始化ArrayList的时候,就应该指定好数组容量,否则当容量不够时就需要扩容和复制,和带来一定的性能损耗。
ArrayList添加元素/扩容机制
当ArrayList add元素时,首先会先确保数组长度是足够的,因此会将数组长度与list中已经存储的元素个数+1进行比较,如果数组长度不够,那么就需要进行扩容。扩容就是将数组长度扩容为原来的1.5倍,然后再将原数组中的元素复制到新数组中。
启发:扩容为原来的1.5倍,底层源码是通过位运算进行计算,比如,假设oldCapacity=13,二进制数是1101,1101 >> 1 = 0110 = 6,6+13=19。扩容后的长度就是19。使用位运算速度更快。
ArrayList怎么实现复制?如何自己实现?
有三种方法,可以使用ArrayList类里的clone方法,这是一个浅拷贝;还可以使用ArrayList类里的addAll方法,这也是一个浅拷贝;还可以使用构造器,将原list作为参数装进构造器里就会得到一个新的list,这也是一个浅拷贝方法。
自己实现的话,首先创建一个新的ArrayList对象,然后依次遍历原ArrayList中的每个元素,将其添加进新ArrayList对象中。在这个过程中还可以手动实现深拷贝。
ArrayList是线程安全的吗?
ArrayList是线程不安全的。举个例子,当调用ArrayList里的add方法时,在多线程的环境下,可能有多个线程拿到同一个size,那么就会将各自的元素添加进数组的同一个位置中,这样就出现了数据覆盖问题,从而导致了线程不安全。
HashMap
HashMap初始化过程
当new一个HashMap时,table数组不会被初始化,只有等第一次put元素时,table数组才会被new出来,如果没有指定哈希表大小的话,则默认table数组长度为16。如果指定了哈希表大小,则会按大于该指定值的2的n次方进行分配。
HashMap put元素过程
首先会根据元素的key通过hashcode方法计算出哈希值,再由哈希值计算出哈希槽的索引位置。
接着判断这个索引位置上是否有元素,如果没有元素则直接我们的新元素插入;
如果这个索引位置上有元素,判断这个元素是否为树结点,
如果是树结点,则走树逻辑;
如果不是,则依次遍历这个索引位置上的链表结点,通过hashcode方法和equals方法判断两个元素的key是否相等。
如果判断出链表上的结点的key与新增元素的key是相等的,则用e指针记录这个重复结点,最后再用新增元素的value替换这 个重复元素的value。
而如果遍历完链表里的所有结点发现都没有重复的,那么就直接在链表结尾插入这个新增元素。
新增完元素后还需要判断哈希表里的元素是否超过阈值,阈值就是哈希表容量*负载因子。如果超过了需要进行扩容。
HashMap扩容过程
根据哈希表容量*负载因子可以得到扩容的阈值。如果哈希表里的元素个数超过了阈值,那么就会进行扩容。扩容是扩容为原来哈希表大小的两倍。然后在将原哈希表上的所有元素重新计算哈希值尾插法迁移至新的哈希表中。
由于都是两倍两倍的扩容,因此我们的哈希表大小一定都是2的n次方。这是为了方便使用位运算计算哈希码对应的数组索引位置。
jdk1.7的HashMap出现的死链问题
死链问题是扩容迁移过程中的头插法导致的。头插法会使链表中元素倒序插入到新表中。如果两个线程同时执行迁移操作,同时执行遍历到某个哈希槽的第一个位置,其中一个线程使得table数组的链表变成倒序,然后另一个线程接着正常执行,那么就会出现指针指回前面的结点,从而出现循环链表。
HashMap出现的线程不安全问题
1、当新增元素时可能出现线程不安全问题
如果两个线程同时执行put元素操作,且都计算出元素所对应的索引下标是同一个位置,然后同时判断出这个索引位置没有元素或者同时循环遍历到链表为尾部,那么就会出现元素覆盖问题,从而导致数据丢失。
2、扩容迁移过程出现的线程不安全问题
情况1:当一个线程执行扩容迁移的过程中,其他线程仍然可以在原表中进行新增元素,如果新增元素落在原表已遍历过的哈希槽上的话,迁移遍历完成后,当table数组引用指向新表时,在原表中新增的元素就会丢失。
情况2:当多个线程都在各自内存中扩容迁移,也就是说它们各自都含有一个新表,当线程迁移完成后,会将新表赋值给共享的table数组,因此就会出现在新表中插入元素被覆盖的问题。