1、重写和重载
重载指的是在同一个类内,对于一个方法重新编写一个不同入参类型/个数/顺序的方法,实现对原方法的重载,这样在编译期创建对象的过程中就会根据入参的情况选择合适的重载方法,为啥只能是参数?因为方法名、参数共同构成了方法签名,返回值不包括在方法签名中,这也是很多类都可以提供不同入参构造器的原因。
重写指的是子类对于父类的方法的重新实现,but方法名、方法参数不能改变,如果返回值是void或者基本类型则可以改变,这相当于在子类中对父类方法进行内容上的重新实现。这里需要注意重写的方法不能是final/private/static,子类返回值须为父类返回值的子类/本类【返回值类型】,并且子类重写后的方法访问范围要比父类大【访问限定】,同时重写后的异常抛出返回不能比父类抛出范围大【异常范围】,使用@Override可以让编译器检查以上三点。
2、面向对象
三大特性:
① 封装:对象中具有的属性对于外部是不可直接访问的,只有通过对象开放的方法才有可能访问到,这样可以避免对象属性被不受限制地修改。也即是说,类的属性字段要声明为private,之后通过提供getter/setter方法来供外部对象进行访问;
② 继承:类A继承类B之后,相当于同样拥有了类B中的属性、方法,这样可以方便对代码进行复用,同时类A对于类B中声明为private的属性是不能访问的,只是拥有了,不等同于破坏了其封装性,类A也可以对类B中的非final/private/static方法进行重写,这个上面提到了;
③ 多态:多态指的是引用类型变量指向的引用类型以及引用变量引起的方法调用在编码时都是无法确定其真实类型的,进而我们可以把子类对象返回用父类类型来接收,子类对父类方法的重写,接口中的多个方法的实现,这些都可以称为多态。
S.O.L.I.D 原则:
单一原则:类或对象最好只承担一种职责;
开闭原则:对扩展开放,对修改关闭;
里氏替换:具备继承关系的两个类,任何出现子类声明的地方都可以用它的父类来替代,要求子类继承父类的时候可以扩展功能,但不能覆盖或者移除父类的功能;
接口分离:一个接口应该只具备一种功能,如果某功能有变不会影响其他功能;
依赖反转:实体应该依赖于抽象而不是实现,高层次的模块不应该依赖于低层次模块。
3、基本类型与包装类型
基本类型所占的字节:
byte 1字节;char 2字节;short 2字节;int 4字节;float 4字节;long 8字节;double 8字节;
boolean比较特殊,jvm在编译期会把boolean转化为int,而boolean数组操作的确实byte数组。
为什么要有包装类型?在Java中基本数据类型可以直接存储在栈中,为了使用面向对象的思想,有了包装类,提供属性和方法,包装类型存储在堆中。
4、装箱拆箱
从基本数据类型到包装类型称为装箱,具体的就是调用包装对象的valueOf方法;从包装类型到基本数据类型称为拆箱,就是调用包装对象的xxxValue方法,xxx代表基本数据类型名。
Java 中有自动装箱和拆箱的机制,但需要注意的是,除了Float和Double之外,其他包装类都会提供装箱拆箱缓存,例如对于Integer来说,处于-128~127之间的数值并不会装箱拆箱,而是指向底层的一个数组,引用同一个对象(享元模式,缓冲池)。此外,算术运算符的出现会导致自动装箱拆箱。
5、定义一个空构造方法
在父类中这样使用为的是避免子类调用无参构造器时因父类不存在无参构造器而报错,同时如果一个类不想被外界通过构造方法实例化,而是想通过指定方法实例化【单例模式】,就可以使用private的空构造器来达成。
6、接口和抽象类
① 两者都不能实例化,两者都应该包含抽象方法;
② 接口中除了final、static之外不能有其他变量,抽象类中无限制;
③ 接口可以多实现,抽象类只能单继承;
④ 接口中不能有方法的实现(jdk8之后又默认方法实现),而抽象类中可以有;
⑤ 接口更多的是对类的约束(要求你必须有什么东西),而抽象类更多的是对多个类相同功能的汇聚,也即是提高代码的可复用性;
⑥ 抽象类是对类的抽象,是一种模板化设计,接口是对行为的抽象,是一种行为的规范;
⑦ 一般情况下接口的使用优于抽象类,毕竟接口的限制约束会少于抽象类;
7、构造方法
方法名与类名一致;不显示提供的话会有默认的无参实现;
没有返回值,不能有返回值类型声明;创建对象实例时自动调用;
8、equals和==
对于基本数据类型来说,==比较的是两者的值,对于引用类型来说, ==比较的是两者引用的对象地址,对于这类对象的比较就需要用到equals。
equals是Object中的方法,默认也是用引用的对象地址进行比较,但子类可以对该方法进行重写,比如String中就对该方法进行重写,通过对底层的char数组逐个进行比较得出比较的结果值,而不是简单的返回引用地址比较结果。
9、equals和hashCode
hashCode方法用于返回该对象的一个散列码信息,这个散列码主要用于在Hash集合中比如HashSet、HashMap之类的容器对对象进行定位,确定存储的位置,由于该方法由对象自行重写实现,因此不同对象的散列码也有可能会相同。此外同一个对象必须保证在对象的生命周期内散列码是不变的。如果没有重写hashCode方法,则调用的是本地方法hashCode,返回的散列码根据对象地址去生成。
这里也就可以得出一个结论:两个对象相等则equals为true,hashCode也相等;两个对象的hashCode一致,不一定就是相等;
重写equals时为什么要重写hashCode呢?
我们可以参照HashSet的键判断原理,对于一个要加入其中的key,会先比较对象本身地址,如果压根就是同个对象也就不用进行后面判断了;之后会根据equals方法和hashCode方法则去比较对象的是否相等。假设现在我们没有重写equals和hashCode,那么仍会是使用Object中的方法去进行比较,相当于比较的还是对象地址是否相等;但假设我们只重写equals方法,现在对象A和对象B值是相等的,那么equals比较之后得到的结果会是true,但由于没有重写hashCode,散列码依旧是对象本身地址的比较,这时候仍会被判定为不相等,接着被加入Set中,这时候就出现了重复元素。
10、final关键字
① “不可变性”:如果一个final变量是基本数据类型,那么初始化后不能改变值;如果是引用类型,那么初始化之后不能指向其他对象;
② 保护性:final修饰的类不能被继承,也就避免了方法重写可能带来的问题,其中的成员方法都会被隐式加上一个final;
③ 锁定:final修饰方法时,可以避免继承的子类修改其定义,类中所有的private方法都被指定为final。
11、String类不可变
为什么不可变?
①String类为final修饰,不可被继承;
②String中对字符串进行修改的方法都是返回新对象,不会影响原来的字符串;
不可变的好处?
① 保证hashCode不被修改,如果不是保证不可变那么可能在字符串加入集合之后,hashCode被修改了,那么这个值就永远也出不来了;
②保证线程安全,因为不能被子类继承,而方法也都不会影响源对象,那么就可以保证线程安全;
③ 有一个叫字符串池的东西,主要是为了重用字符串,如果字符串是可变的,那么牵一发则动全身;
④ 考虑String经常被作为参数传递,比如连接信息等,如果创建连接之后又被修改字符串值,那么就会出错。
12、异常处理
1)异常的分类
Throwable派生出Exception(异常)和Error(错误),前者可以被try catch,后者无法处理只能避免。
Exception中分为受检查和不受检查,前者必须try catch处理,后者可以不处理。
除了RuntimeException及其子类之外,其他异常都属于受检查异常,必须在程序中处理。
RuntimeException包括NullPointException(NPE),ArrayIndexOutOfBoundsException(数组越界),ClassCastException(强转异常)等。
2)finally
finally在以下情况不会运行:CPU停止运行、线程死亡、try/catch中调用了System.exit() 方法,其他情况下必定会执行。
如果try和finally中都有return,那么finally中的会被执行,返回值被其替换。
12、序列化
如果有字段不想被序列化可以加上transient关键字修饰,其可以阻止实例中用此关键字修饰的变量进行序列化,同样也就无法被反序列化回来,该关键字只能加在变量上。
13、获取输入
1)Scanner
Scanner s = new Scanner(System.in);
String next = s.nextLine();
// 使用完毕需要关闭
s.close()
2)BufferedReader
BufferedReader bf = new BufferedReader(new InputStreamReader(System.in));
String next = bf.nextLine();
14、IO流
按流向:输入流,输出流;
按操作单元:字节流,字符流;
按角色:节点流,处理流;
为什么有了字节流还有有字符流?
虽然在信息传输中最小单元都是字节,但是如果收到字符信息,在Java中是把字节流转换为字符操作,这个过程有一定的性能消耗,因此有了字符流专门针对字符信息处理传输,而视频、图片等媒体文件则通过字节流传输。
15、IO模型
BIO:同步阻塞IO,一个连接发起IO后必须等待回复之后才可以进行其他操作,数据的读取和写入在一个线程中处于阻塞的状态;
NIO:同步非阻塞IO,支持面向缓存基于通道的IO方法,可以在不阻塞IO的情况下进行数据传输,主要就是把数据交给类似分发器的东西,让它去轮询进行IO操作;
AIO:异步非阻塞IO,基于事件和回调机制实现,读取/输入事件发生后不等待结果,而是由操作系统去通知线程进行操作。
16、深浅拷贝
浅拷贝:对于基本数据类型进行值传递,对于引用类型只传递引用对象,后续他们操作的是同一个对象;
深拷贝:对于基本数据类型进行值传递,对于引用类型拷贝一份所引用到的对象,传递过去,这样他们后续操作的就不是同一个对象了。
17、集合—ArrayList
关于初始化,分三种情况:
① 空参:底层构造数组长度为0,后续添加元素再进行扩容【懒加载】;
② 带容量参数:直接根据该容量创建对应长度的数组;
③ 传入集合:如果集合不为空,则先将集合转数组,之后根据集合大小创建底层数组,并将集合数组元素搬运到底层数组;
关于添加元素,分两种情况:
① add:第一步,进入计算容积,如果底层数组为空,则将容量值设置为默认容量(10),否则容量值记为size+1;第二步,如果第一步计算出的容量值大于底层数组长度,进入扩容;第三步,如果是初次扩容则扩大到默认容量,否则将数组容积扩大为原来的1.5倍。
② addAll:基本步骤同add,不同的是首次添加时,如果集合元素个数小于默认容量,则按照默认容量扩容;否则按照集合元素个数+1扩容。
18、ArrayList和LinkedList对比
1)ArrayList
① 底层基于动态数组,存储时需要连续的内存,这也就导致了扩容的时候需要重新找到一块完整连续的内存并移动元素过去;
② 由于是连续内存,因此可以通过下标去进行访问,这里只是偏移量的增减速度快;
③ 也正是因为连续存储,在尾部添加元素的效率是很快的,但如果在其他位置插入则或多或少会涉及元素的迁移,效率也拉满了;
④ 同时这里考虑计算机的局部性原理,在加载一块内存进寄存器时会同时把后续连续的内存也加载进去,这也就惠及了ArrayList的连续存储特性,可能一次加载就包括了很多个元素。
由此可见,Arraylist的特点/优点基本上都是基于其连续存储的特性来的。
2)LinkedList
① 底层基于双向链表,存储时无需连续内存,相应地也会有内存碎片化的问题;
② 由于并非连续存储,因此无法根据下标访问,寻找元素时需要一步一步跳;
③ 在头尾插入元素性能会快,因为一般会存储头尾指针,直接修改指针即可,如果是在其他位置插入同样需要遍历元素;
④ 内存占用方面,由于是双向链表,需要维护一个prev和next指针,这也就导致了内存一定程度上的增加,相比于ArrayList的纯数据。
19、HashMap
1)底层数据结构
JDK1.7 :数组+链表【头插法】
JDK1.8:数组+链表【尾插法】+红黑树
JDK1.8中,底层使用数组来存储k-v的Node,默认初始容量为16,如果同个位置出现多个元素,则拉链处理,采用尾插法进行插入;
2)JDK1.8中链表与红黑树
何时树化?当链表长度达到阈值8并且数组长度小于64时,先进行数组扩容;如果数组长度大于等于64并且触发链表阈值,则会将链表转化为红黑树。
为什么要转树? 极端情况下,一个位置上的链表会拉的很长,查找元素就退化成了O(N)的复杂度;因此在链表到达一定阈值时需要转红黑树,转树之后查询修改复杂度都为O(logN)。
为什么不一开始就转树? 正常情况下,hash表的查询都是O(1),如果一开始就引入红黑树,反而会导致查询时间复杂度下降到O(logN),并且转树应该是极端情况下才会出现。
为什么链表树化阈值是8? 在hash函数设计得当的情况下,hash值在表内是遵循泊松分布的,同位置上链表长度为8的概率是亿分之六,那么设置成8可以避免一些不必要的树化操作,同时达到8之后可以树化,相当于多了个兜底策略。
什么时候树会退化成链表? ①扩容时如果树被拆分到元素小于等于6个,那么会转化为链表;②如果remove操作时发现树的 root、root.left、root.right、root.left.left 其中一个为null,那么也会退化成链表。
3)初始化
① 无参:只会初始化负载因子;
② 带初始容量:带默认负载因子进入初始化方法③;
③ 带初始容量和负载因子:判断容量和负载因子的合法性,假设传入的初始容量是N,则调用tableSizeFor方法,获取离N最近的2的次幂数【大于等于N】,将其设置为下次扩容阈值;
4)容量与扩容
JDK1.8中,默认初始容量是16,负载因子默认是0.75,扩容阈值为容量*负载因子。容量到达阈值后下一次加入操作就会导致扩容,扩容为原来的两倍。
负载因子为什么是0.75? 负载因子用于控制数组中元素的稀疏,如果趋近1那么可能导致链表过长,虽然空间利用率上去了,但是会导致查询的效率低下;如果趋近0那么会导致频繁扩容,查询效率上去了,空间利用率下来了。从源码中的注释看,这个取值可以很好地平衡时间和空间上的开销。
5)添加元素的操作
怎么计算数组索引? 先计算对象的hashcode,再进行二次hash,最后 & (容量 - 1)。
为什么还要二次hash? 综合高位的数据,让哈希分布更均匀。
为什么数组容量要是2的次幂? 2的次幂可以方便进行位运算,将例如计算索引时将取模的操作替换为按位与操作,提升了效率;此外在扩容元素迁移时,可以使用按位与操作来判断元素是否应该迁移到新数组。
JDK1.7和1.8的put操作有什么不同? 在链表节点插入方面,1.7采用头插法,1.8改用尾插法;扩容方面,1.7是大于等于阈值并且插入的位置没空位才扩容,1.8是大于阈值就扩容;
6)其他问题
线程安全问题:1.7中HashMap的头插法会导致多线程下的死循环问题,并且无论是1.7还是1.8,HashMap都是线程不安全的,可能会导致数据错乱;
key选值问题:HashMap的key可以为null,但只能有一个;作为key的对象应该重写hashcode和equals方法,并保证不可变;
20、单例模式
// 单例一 饿汉式 防止反射破坏 防止反序列化破坏
class Singleton1 {
private static final Singleton1 INSTANCE = new Singleton1();
// 防止反射破坏
private Singleton1() {
if (INSTANCE != null) {
throw new RuntimeException("单例对象不能反复创建");
}
}
public static Singleton1 getInstance() {
return INSTANCE;
}
// 防止反序列化破坏
public Singleton1 readResolve() {
return INSTANCE;
}
}
// 单例二 饿汉式枚举类单例 天然防止反射 反序列化破坏
enum Singleton2 {
INSTANCE;
Singleton2() {
System.out.println("Singleton2构造方法被调用了");
}
public static Singleton2 getInstance() {
return INSTANCE;
}
@Override
public String toString() {
return "Singleton2{} " + INSTANCE.hashCode();
}
}
// 单例三 懒汉式 为确保线程安全 在获取对象方法上加synchronized
class Singleton3 {
private static Singleton3 INSTANCE;
// 防止反射破坏
private Singleton3() {
System.out.println("Singleton3构造方法被调用了");
if (INSTANCE != null) {
throw new RuntimeException("单例对象不能反复创建");
}
}
public static synchronized Singleton3 getInstance() {
if (INSTANCE == null) {
INSTANCE = new Singleton3();
}
return INSTANCE;
}
// 防止反序列化破坏
public Object readResolve() {
return INSTANCE;
}
}
// 单例四 减小锁粒度 DCL 双检锁
class Singleton4 {
private volatile static Singleton4 INSTANCE;
private Singleton4() {
if (INSTANCE != null) {
throw new RuntimeException("单例对象不能重复创建");
}
System.out.println("Singleton4单例对象被创建了");
}
public static Singleton4 getInstance() {
if (INSTANCE == null) {
synchronized (Singleton4.class) {
if (INSTANCE == null) {
INSTANCE = new Singleton4();
}
}
}
return INSTANCE;
}
public Object readResolve() {
return INSTANCE;
}
}
// 单例五 静态内部类 无需DCL
class Singleton5{
private Singleton5() {
System.out.println("Singleton5单例对象被创建了");
}
private static class GenerateInstance{
static Singleton5 INSTANCE = new Singleton5();
}
public static Singleton5 getInstance() {
return GenerateInstance.INSTANCE;
}
}