JAVA
JAVA的一些基础点和底层原理总结
面向对象
面向对象将问题分解成一个一个步骤,对每个步骤进行相应的抽象,形成对象,通过不同对象之间的调用,组合解决问题。
就是说,在进行面向对象进行编程的时候,要把属性、行为等封装成对象,然后基于这些对象及对象的能力进行业务逻辑的实现。比如想要造一辆车,上来要先把车的各种属性定义出来,然后抽象成一个Car类。
面向对象有封装、继承、多态三大基本特征,和单一职责原则、开放封闭原则、Liskov替换原则、依赖倒置原则和 接口隔离原则等五大基本原则。
封装
封装就是把现实世界中的客观事物抽象成一个Java类,然后在类中存放属性和方法。如封装一个学生类,其中包含了姓名,年龄,家庭地址等属性,并且有学习,玩耍等方法。
继承
像现实世界中儿子可以继承父亲的财产、样貌、行为等一样,编程世界中也有继承,继承的主要目的就是为了复用。子类可以继承父类,这样就可以把父类的属性和方法继承过来。 如Dog类可以继承Animal类,继承过来嘴巴、颜色等属性, 吃东西、奔跑等行为。
多态
多态是指在父类中定义的属性和方法被子类继承之后,可以通过重写,使得父类和子类具有不同的实现,这使得同一个属性或方法在父类及其各个子类中具有不同含义。
接口和抽象类
我们平时也提倡面向接口开发,在接口中制定规范,在业务层面实现接口进行业务逻辑开发。
如果多个实现类中有相同重复的代码,可以在接口和实现类中添加一个抽象类,将公用的代码抽取出来。
接口
接口只是对一类事务的属性和行为进行更高层次的抽象,它对修改关闭,但是对扩展(不同的实现 implement)开放,接口是开闭原则的一种体现。
因为对扩展开放,所以接口的方法默认都是public abstract;
接口中不可以定义变量,所以接口的属性默认都是public static final常量,且必须赋初始值。
接口的职责是为了规范,我们平时也提倡面向接口开发
抽象类
抽象类的主要是为了复用代码,比较典型的就是模版方法模式
异常
Throwable
Throwable是JAVA中最顶级的异常类,继承自Objecet类。
Exception
Exception是一些代码层面的可预见的可以进行捕获处理的异常
受检异常
受检异常必须要在代码中显式声明并且有对应的处理,比如捕获或者向上抛出。否则编译无法通过,一般是IO操作多一些
非受检异常
非受检异常多是一些代码层面的问题,继承自RuntimeException,不显式的去声明,但是一旦发生,程序就会中断。
常见的RuntimeException
- 索引越界异常
- 空指针异常
- 算数异常
- 非法参数异常
- 类转换异常
Error
Error是程序系统或者虚拟机发生严重的错误,比如OOM(内存溢出),SOE(栈溢出)
final
final修饰变量,表示这个变量是最终的,不能被更改
final修饰类,表示这个类不能被继承
final修饰方法,表示该方法不能被子类重写
用final修饰的对象,表示这个对象的引用地址是不可变的,但是对象的内容是可以修改的。例如,final MyClass obj = new MyClass();
将 obj 声明为⼀个不可变引⽤,指向⼀个可变的 MyClass 对象。
1、final可以⽤来修饰的结构:类、⽅法、变量
2、final⽤来修饰⼀个类:此类不能被其它类继承。当我们需要让⼀个类永远不被继承,此时就可以⽤final修饰,但要注意:final类中所有的成员⽅法都会隐式的定义为final⽅法。⽐如:String类、System类、StringBuffer类
3、final ⽤来修饰⽅法 :表明此⽅法不可以被重写
作⽤:
(1) 把⽅法锁定,以防⽌继承类对其进⾏更改。
(2) 效率,在早期的java版本中,会将final⽅法转为内嵌调⽤。但若⽅法过于庞⼤,可能在性能上不会有多⼤提升。因此在最近版本中,不需要final⽅法进⾏这些优化了。
final⽅法意味着“最后的、最终的”含义,即此⽅法不能被重写。
⽐如:Object类中的getClass( )
4、final ⽤来修饰变量 ,此时变量就相当于常量
- final⽤来修饰属性:可以考虑赋值的位置有:显式初始化、代码块中初始化、构造器中初始化
- final修饰局部变量:尤其是使⽤final修饰形参时,表明此形参是⼀个常量。当我们调⽤此⽅法时,给常量形参赋⼀个实参,⼀旦赋值之后,就只能在⽅法体内使⽤此形参的值,不能重新进⾏赋值。
- 如果final修饰⼀个引⽤类型时,则在对其初始化之后便不能再让其指向其他对象了或者说他的地址不能发⽣变化了(因为引⽤的值是⼀个地址,final要求值,即地址的值不发⽣变化),但该引⽤所指向的对象的内容是可以发⽣变化的。本质上是⼀回事。
5、使⽤ final 关键字声明类、变量和⽅法需要注意以下⼏点:
- final ⽤在变量的前⾯表示变量的值不可以改变,此时该变量可以被称为常量。
- final ⽤在⽅法的前⾯表示⽅法不可以被重写(⼦类中如果创建了⼀个与⽗类中相同名称、相同返回值类型、相同参数列表的⽅法,只是⽅法体中的实现不同,以实现不同于⽗类的功能,这种⽅式被称为⽅法重写,⼜称为⽅法覆盖。这⾥了解即可,教程后⾯我们会详细讲解)。
- final ⽤在类的前⾯表示该类不能有⼦类,即该类不可以被继承。
- final 修饰变量
final 修饰的变量即成为常量,只能赋值⼀次,但是 final 所修饰局部变量和成员变量有所不同。
final 修饰的局部变量必须使⽤之前被赋值⼀次才能使⽤。
final 修饰的成员变量在声明时没有赋值的叫“空⽩ final 变量”。空⽩ final 变量必须在构造⽅法或静态代码块中初始化。
注意:final 修饰的变量不能被赋值这种说法是错误的,严格的说法是,final 修饰的变量不可被改变,⼀旦获得了初始值,该 final 变量的值就不能被重新赋值。
包装类
因为JAVA是面向对象的语言,很多场景基本数据类型都不适用,比如创建集合容器要求的就是Object的对象。
为了让基本数据类型也拥有对象的特征,将基本数据类型封装起来,对其添加了属性和方法,方便我们在一些场景下使用
JAVA中提供了基本数据类型和包装类的自动拆装箱机制,简化了我们的很多操作
hashCode 和 equals ⽅法
hashCode方法用于快速比较两个对象是否不相同
hashCode方法会对两个对象的引用内存地址进行hash运算,返回一个hash值,如果两个对象的hash值不相等,那么这两个一定不是同一个对象。如果相等的话,因为hash碰撞的原因,其实不一定可以说明这两个是相同的对象
equals方法底层默认还是使用“==”符号来进行判断,返回布尔值。
equals方法底层是“==”,对于基本数据类型判断他们在内存的方法区的值是否相等,对于引用数据类型,判断他们的内存地址是否相等。如果面对更负责的需求场景,就需要重新equals方法进行具体的判断。
具体来说:
hashCode和equals⽅法是Object类的两个重要⽅法,⽤于判断对象的相等性和⽣成对象的哈希值。
hashCode:
可以这样理解:它返回的就是根据对象的内存地址换算出的⼀个值。
== :
== ⽐较的是变量(栈)内存中存放的对象的(堆)内存地址,⽤来判断两个对象的地址是否相同,
即是否是 指相同⼀个对象。⽐较的是真正意义上的指针操作。
- ⽐较的是操作符两端的操作数是否是同⼀个对象。
- 两边的操作数必须是同⼀类型的(可以是⽗⼦类之间)才能编译通过。
- ⽐较的是地址
equals:
equals⽤来⽐较的是两个对象的内容是否相等,由于所有的类都是继承⾃java.lang.Object类的,所以 适⽤于所有对象,如果没有对该⽅法进⾏覆盖的话,调⽤的仍然是Object类中的⽅法,⽽Object中的 equals⽅法返回的却是==的判断。
总结:
如果两个对象equals相同,hashCode⼀定相同
如果两个对象equals不同,hashCode不⼀定不同
如果两个对象的hashCode相同,它们的equals并不⼀定相同
如果两个对象的hashCode不相同,它们的equals⼀定不相同
所有⽐较是否相等时,都是⽤equals ,并且在对常量相⽐较时,把常量写在前⾯,因为使⽤object的 equals object可能为null 则空指针 在阿⾥的代码规范中只使⽤equals ,阿⾥插件默认会识别,并可以快速修改,推荐安装阿⾥插件来排 查⽼代码使⽤“==”,替换成equals
数值精度问题
为什么不能用浮点数来表示金额
浮点数表示的是一个近似值,用浮点数来表示金额会造成精度丢失的问题
可以用JAVA中提供的BigDecimal来表示金额
在BigDecimal中,equals和compareTo
Bigdecima的equals方法会比较两个数的值和标,比如0.1和0.10就会返回false
compareTo方法做了优化,忽略了标的比较,只比较值的大小
在使用BigDecimal时,为了避免精度丢失问题,优先使用参数为String的构造方法
String,StringBuffer和StringBuilder
String是只读字符串,从底层源码来看是一个final类型的字符数组,一旦定义,无法再增删改,每次对String的操作都会生成新的String对象。所以在进行频繁的字符串操作时,建议使用StringBuffer和StringBuilder来进行操作。另外StringBuffer 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。StringBuilder 并没有对方法进行加同步锁,所以是非线程安全的。
String
String是不可变的,线程安全的
String为什么设计为不可变的?
- 线程安全
- 安全性 :字符串可以用来保存url地址,密码等敏感信息,设计为不可变可以保障这些信息的安全性
- 缓存 ,提高性能 :字符串是使用最广泛的数据类型,大量的创建非常耗费资源,所以JAVA在堆内存中维护了一个字符串常量池用来缓存,大大节省了堆内存的空间。两个相同内容的字符串变量可以指向字符串常量池中的同一个字符串对象。
- 可以作为Hash结构中的key
StringBuffer
StringBuffer的对象是可变的,是线程安全的,但是效率比StringBuilder低
StringBuilder
StringBuilder的对象是可变的,但不是线程安全的,效率较高
反射
反射可以在运行期间动态的获取到一个类的属性和方法
反射就是 Reflection,Java 的反射是指程序在运⾏期可以拿到⼀个对象的所有信息。正常情况下,如果我们要调⽤⼀个对象的⽅法,或者访问⼀个对象的字段,通常会传⼊对象实例
为什么需要反射
-
一个类在程序运行的时候从云端下载下来,或者从其他文件加载进来,那么我们如何使用这个类呢,这个时候就需要用到反射。
-
程序为了封装性,会尽量暴露最少的信息给外部类使用,从技术的角度来讲这是没错的。但是需求总是改变的(还记得根据手机壳的颜色来改变APP的主题),所以有时候我需要更大的权限,这个时候就需要使用反射。
-
JAVA的动态代理:
JDK动态代理:
- JDK动态代理是通过反射来实现的 -
- JDK动态代理面向的是实现了接口的类
CGLIB动态代理
- CGLIB动态代理是通过继承来实现的
- CGLIB动态代理不能代理被final修饰的类
Spirng 默认采⽤ JDK 动态代理实现机制
反射的缺点
使用反射毕竟它要去动态的解析一个类,所以程序运行的性能会降低一些。
反射打破了我们代码的封装性,增加了维护成本。
try-catch-finally
finally中的代码在try-catch块中的代码执行完毕后一定会执行,无论是否发生异常。即使try或catch块中使用了return语句,finally块中的代码也会在return执行前执行。也就是说,finally块中的代码会在函数返回之前执行。
什么情况下finally代码块不会执行
try或者catch中执行了System.exit(),程序退出
程序发生宕机崩溃
JAVA中是值传递还是引用传递
编程语言中需要进行方法之间的参数传递,这个传递策略就是求值策略
值传递和引用传递就是比较常用的两种求值策略
值传递和引用传递最大的区别就是是否复制出来一个副本进行传递,如果有,那么就是值传递。否则就是引用传递
JAVA中的求值策略是值传递,只不过是把对象的引用关系复制了一份来进行传递。
JAVA中的对象传递,如果是修改引用,则不会对原来的对象产生影响。如果是修改共享对象的属性的值,是会对原来的对象有影响的。
克隆
为什么需要克隆
一般是需要创建一个相同的对象需要较多的资源,使用克隆可以节省资源,提高效率
如何实现克隆
通过colonable接口实现克隆
深克隆和浅克隆
浅拷贝是复制了对象的基本数据类型的值和引用数据类型的内存地址,当这一个对象修改引用数据类型时,另一个对象也会受到影响
深拷贝是完全独立开辟了一个新的内存空间,复制了对象的所有属性和元素到新的对象中,两个对象之间不会互相影响
集合
ArrayList和LinkedList
ArrayList
ArrayList底层的结构是数组,随着元素的添加,可以动态的改变数组的大小。通过get()和set()方法可以可以访问其内部的元素
ArrayList动态扩容
- ArrayList初始容量为零,当第一次添加数据时,容量会初始化为10
- 添加元素的时候会先判断是否会超出数组容量
- 如果超出容量,则会按照旧数组的容量创建一个1.5倍容量大小的新数组
- 将旧数组的元素Copy给新数组,然后将新元素添加进来
LinkedList
LinkedList底层的结构是双向链表,在添加和删除操作的性能要高于ArrayList,但是在查找和更新的操作的性能低于ArrayList。
ArrayList和LinkedList都不是线程安全的,如果要使用线程安全的List
-
在方法内使用,局部变量是线程安全的
-
则需要使用Collections工具类的synchronizedList方法来创建List对象,它会对所有读写操作进行同步处理,保证多线程环境下的线程安全。
数组和List互相转换
数组转换成LIst:调用Arrays.asList方法
类似于浅克隆,修改数组的内容,list会受到影响
因为在底层,只不过是使用Arrays类下的ArrayList内部类进行了封装,指向的是同一个引用地址
List装换成数组:调用List.toArray方法
类似于深克隆,修改List内容,数组不会受到影响
因为在底层进行了数组拷贝,创建了一个新的数组。
HashMap
实现原理
HashMap的底层数据结构是哈希表,就是数组加链表或者红黑树的结构。添加数据的时候,对Key进行哈希运算计算出一个哈希值来确定数据在数组中的下标,如果发生哈希冲突,计算出相同的哈希值,就采用拉链法,将数据存放到数组连接的链表中。如果某一个数组元素下的链表长度大于8并且将数组长度大于64时,就将链表转换为红黑树。在扩容的时候,如果红黑树拆分的树的结点数小于6时,就退化为链表。
在JDK1.7版本的时候,HashMap的底层只是数组加链表,并没有红黑树。
链表的时间复杂度是O(n)
红黑树的时间复杂度是O(logn)
添加元素
逻辑图链接https://www.processon.com/embed/65439a0a838abe10a52f8693
扩容
在初始化的时候,会创建一个长度为16的数组,HashMap的扩容是在判断数组的长度是否超过阈值时进行的,这个阈值是数组的长度乘以加载因子(0.75),当超过这个阈值时,会新建一个两倍长度的数组,遍历原数组上的每一个值,并且重新计算它们在新数组上的位置,将原来数组上的元素都存储到新数组对应的位置上。这个新数组就变成了HashMap的底层数据结构。
加载因子: HashMap的加载因子默认是0.75,这是一个经验上较为理论的值,最大程度的平衡了空间利用率和时间复杂度。
通过扩容,HashMap可以减少链表长度,提高查询、插入和删除操作的效率。
Hash冲突如何解决
- 采用拉链法 : 在数组下连接链表
- 开放寻址法: 发生哈希冲突时,就去寻找下一个空的散列地址进行存入
- 再哈希法 : 发生哈希冲突时,使用新的Hash函数计算哈希值,直到不产生冲突为止
HashMap不是线程安全的
如何保证线程安全
ConcurrentHashMap
在JDK 1.7中,ConcurrentHashMap使用了分段锁技术,即将哈希表分成多个段,每个段拥有一个独立的锁。这样可以在多个线程同时访问哈希表时,只需要锁住需要操作的那个段,而不是整个哈希表,从而提高了并发性能。
在JDK 1.8中,ConcurrentHashMap的实现方式进行了改进,使用分段锁和“CAS+Synchronized”的机制来保证线程安全。在JDK 1.8中,ConcurrentHashMap会在添加或删除元素时,首先使用CAS操作来尝试修改元素,如果CAS操作失败,则使用Synchronized锁住当前槽,再次尝试put或者delete。这样可以避免分段锁机制下的锁粒度太大,以及在高并发场景下,由于线程数量过多导致的锁竞争问题,提高了并发性能。
Set
Set保证元素不重复
在Java的Set体系中,根据实现方式不同主要分为两大类。HashSet和TreeSet。TreeSet 是二叉树实现的,TreeSet中的数据是自动排好序的,不允许放入null值;底层基于TreeMapHashSet 是哈希表实现的,HashSet中的数据是无序的,可以放入null,但只能放入一个null,两者中的值都不能重复,就如数据库中唯一约束;底层基于HashMap