基本类型和包装类型的区别?
区别 | Integer | int |
---|---|---|
初始值 | null | 0 |
存储位置 | 堆 | 栈 |
用于泛型 | 可用于 | 可以 |
占用空间 | 较大 | 较小 |
方法 | 封装了方法,更灵活 | 无 |
为什么有包装类型?Java是面向对象的嘛,集合里面只能存储对象
private
则子类中就不是重写。发生在运行期1、调用方式
在外部调用静态方法时,可以使用 类名.方法名
的方式,也可以使用 对象.方法名
的方式,而实例方法只有后对象.方法名
这种方式。也就是说,调用静态方法可以无需创建对象 。
2、访问类成员是否存在限制
静态方法在访问本类的成员时,只允许访问静态成员(即静态成员变量和静态方法),不允许访问实例成员(即实例成员变量和实例方法),而实例方法不存在这个限制。
public
,private
,static
等修饰符所修饰,而局部变量不能被访问控制修饰符及 static
所修饰;但是,成员变量和局部变量都能被 final
所修饰。
static
修饰的,那么这个成员变量是属于类的,如果没有使用 static
修饰,这个成员变量是属于实例的。而对象存在于堆内存,局部变量则存在于栈内存。
final
修饰的成员变量也必须显式地赋值),而局部变量则不会自动赋值。
String
类被 final
修饰导致其不能被继承,进而避免了子类破坏 String
不可变。final
修饰且为私有的,并且String
类没有提供/暴露修改这个字符串的方法。1.可变性
String
不可变,StringBuilder
和StringBuffer
是可变的2.线程安全性
String
由于是不可变的,所以线程安全。
StringBuffer
对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。
StringBuilder
并没有对方法进行加同步锁,所以是非线程安全的。
3.性能
StringBuilder
> StringBuffer
> String
Object 类是一个特殊的类,是所有类的父类。它主要提供了以下 11 个方法:
/**
* native 方法,用于返回当前运行时对象的 Class 对象,使用了 final 关键字修饰,故不允许子类重写。
*/
public final native Class<?> getClass()
/**
* native 方法,用于返回对象的哈希码,主要使用在哈希表中,比如 JDK 中的HashMap。
*/
public native int hashCode()
/**
* 用于比较 2 个对象的内存地址是否相等,String 类对该方法进行了重写以用于比较字符串的值是否相等。
*/
public boolean equals(Object obj)
/**
* naitive 方法,用于创建并返回当前对象的一份拷贝。
*/
protected native Object clone() throws CloneNotSupportedException
/**
* 返回类的名字实例的哈希码的 16 进制的字符串。建议 Object 所有的子类都重写这个方法。
*/
public String toString()
/**
* native 方法,并且不能重写。唤醒一个在此对象监视器上等待的线程(监视器相当于就是锁的概念)。如果有多个线程在等待只会任意唤醒一个。
*/
public final native void notify()
/**
* native 方法,并且不能重写。跟 notify 一样,唯一的区别就是会唤醒在此对象监视器上等待的所有线程,而不是一个线程。
*/
public final native void notifyAll()
/**
* native方法,并且不能重写。暂停线程的执行。注意:sleep 方法没有释放锁,而 wait 方法释放了锁 ,timeout 是等待时间。
*/
public final native void wait(long timeout) throws InterruptedException
/**
* 多了 nanos 参数,这个参数表示额外时间(以毫微秒为单位,范围是 0-999999)。 所以超时的时间还需要加上 nanos 毫秒。。
*/
public final void wait(long timeout, int nanos) throws InterruptedException
/**
* 跟之前的2个wait方法一样,只不过该方法一直等待,没有超时时间这个概念
*/
public final void wait() throws InterruptedException
/**
* 实例被垃圾回收器回收的时候触发的操作
*/
protected void finalize() throws Throwable { }
== 对于基本数据类型,比较的是值;对于引用数据类型,比较的是内存地址。
equals 对于没有重写equals方法的类,equals方法和==作用类似;对于重写过equals方法的类,equals比较的是值。
因为两个相等的对象的 hashCode
值必须是相等。也就是说如果 equals
方法判断两个对象是相等的,那这两个对象的 hashCode
值也要相等。
equals()
时没有重写 hashCode()
方法的话就可能会导致 equals
方法判断是相等的两个对象,hashCode
值却不相等。在HashSet
中,会导致都能添加成功,那么HashSet
中会出现很多重复元素,HashMap
也是同理(因为HashSet
的底层就是通过HashMap
实现的),会出现大量相同的Key
。所以重写equals
方法后,hashCode
方法也必须重写。
hashCode
值不同,则它们一定不相等,所以先计算对象的hashCode
值可以在一定程度上判断两个对象是否相等,提高了集合的效率。
总结一下,一共两点:第一,在HashSet
等集合中,不重写hashCode
方法会导致其功能出现问题;第二,可以提高集合效率。
在Java语言中,abstract class
和interface
是支持抽象类定义的两种机制。
抽象类:用来捕捉子类的通用特性的,用于代码复用。接口:抽象方法的集合,用于对类的行为进行约束。
相同点:
不同点:
类型 | 抽象类 | 接口 |
---|---|---|
定义 | abstract class | Interface |
实现 | extends | implements |
继承 | 抽象类可以继承一个类和实现多个接口;子类只可以继承一个抽象类 | 接口只可以继承多个接口;子类可以实现多个接口 |
变量 | 访问修饰符默认是 default,可以是public、protected | 只能是public static final |
方法 | public、protected和default | public abstract、default |
构造器 | 抽象类可以有构造器 | 接口不能有构造器 |
这些概念看着比较枯燥,可以从这个经典的烧开水的例子去理解
BIO :来到厨房,开始烧水,并坐在水壶面前一直等着水烧开。
NIO:来到厨房,开始烧水,不一直坐在水壶前面等,而是做些其他事,然后每隔几分钟到厨房看一下水有没有烧开。
AIO:来到厨房,开始烧水,不一直坐在水壶前面等,而是在水壶上面装个开关,水烧开之后它会通知我。
泛型提供编译时类型安全检测机制,通过泛型参数可以指定传入的对象类型,编译时可以对泛型参数进行检测 |
---|
泛型擦除:泛型信息只存在于代码编译阶段,在进入 JVM 之前,与泛型相关的信息会被擦除掉。Java编译器是通过先检查代码中泛型的类型,然后在进行类型擦除,再进行编译。 |
编译时,检查添加元素的类型,更安全,减少了类型转换次数,提高效率。比如原生的List返回类型是Object对象,需要手动转换类型才能使用,使用泛型后编译器自动转换 |
泛型类、泛型接口、泛型方法 |
支持通配符 <?> :支持任意泛型类型 <? extends A>:支持A类以及A类的子类,规定了泛型的上限 <? super A>:支持A类以及A类的父类,不限于直接父类,规定了泛型的下限 |
构建集合工具类,自定义接口通用返回结果、excel导出类型 |
通过反射可以运行时获取任意一个类的所有属性和方法,还可以调用这些方法和属性。 |
---|
Spring/Spring Boot、MyBatis 等等框架中都大量使用了反射机制。 这些框架中也大量使用了动态代理,而动态代理的实现也依赖反射。 |
优点:运行期类型的判断,动态加载类,提高代码灵活度。 |
缺点:使用反射基本是解释执行,对执行速度有影响。安全问题。比如可以无视泛型参数的安全检查 |
主要用于修饰类、方法或者变量,提供某些信息供程序在编译或者运行时使用。 |
---|
注解只有被解析之后才会生效,常见的解析方法有两种: |
编译期直接扫描 :编译器在编译 Java 代码的时候扫描对应的注解并处理,比如某个方法使用@Override 注解,编译器在编译的时候就会检测当前的方法是否重写了父类对应的方法。 |
运行期通过反射处理 :像框架中自带的注解(比如 Spring 框架的 @Value 、@Component)都是通过反射来进行处理的。 |
序列化: 将数据结构或对象转换成二进制字节流的过程 |
---|
反序列化:将在序列化过程中所生成的二进制字节流转换成数据结构或者对象的过程 |
序列化的主要目的是通过网络传输对象或者说是将对象存储到文件系统、数据库、内存中。 |
序列化协议对应于 TCP/IP 4 层的应用层,对应于7 层的表示层 |
为什么不推荐使用 JDK 自带的序列化? |
不支持跨语言调用 : 如果调用的是其他语言开发的服务的时候就不支持了。 |
性能差 :相比于其他序列化框架性能更低,主要原因是序列化之后的字节数组体积较大,导致传输成本加大。 |
存在安全问题 :序列化和反序列化本身并不存在问题。但当输入的反序列化的数据可被用户控制,那么攻击者即可通过构造恶意输入,让反序列化产生非预期的对象,在此过程中执行构造的任意代码。 |
专门提供给服务提供者或者扩展框架功能的开发者去使用的一个接口。 |
---|
SPI 将服务接口和具体的服务实现分离开来,将服务调用方和服务实现者解耦,能够提升程序的扩展性、可维护性。修改或者替换服务实现并不需要修改调用方。 |
很多框架都使用了 Java 的 SPI 机制,比如:Spring 框架、数据库加载驱动、日志接口、以及 Dubbo 的扩展实现等等。 |
SPI 的优缺点? |
通过 SPI 机制能够大大地提高接口设计的灵活性,但是 SPI 机制也存在一些缺点,比如: |
需要遍历加载所有的实现类,不能做到按需加载,这样效率还是相对较低的。 |
当多个 ServiceLoader 同时 load 时,会有并发问题。 |
语法糖 代指的是编程语言为了方便程序员开发程序而设计的一种特殊语法。实现相同的功能,基于语法糖写出来的代码往往更简单简洁且更易阅读。
Java 中最常用的语法糖主要有泛型、自动拆装箱、增强 for 循环、try-with-resources 语法、lambda 表达式、变长参数、枚举、内部类等。
foreach底层是怎么实现的
foreach和for有什么区别?
lambda 表达式、Stream流式编程、新时间日期 API、接口默认方法与静态方法
Object[]
存储,线程不安全,有预留的内存空间双向链表
数据结构,线程不安全,没有预留的内存空间,不可通过序号快速获取对象,但每个节点都有两个指针占用了内存Object[]
存储,线程不安全NullPointerException
。默认初始容量为11,每次扩容变为原来的2n+1。创建时给定了初始容量,会直接用给定的大小。它基本被淘汰了,要保证线程安全可以用ConcurrentHashMap。
TreeMap
和HashMap
都继承自AbstractMap
,TreeMap
它还实现了NavigableMap
接口和SortedMap
接口。
NavigableMap
接口让 TreeMap
有了对集合内元素的搜索的能力。
SortedMap
接口让 TreeMap
有了对集合中的元素根据键排序的能力。默认是按 key 的升序排序,不过我们也可以指定排序的比较器。
相比于HashMap
来说 TreeMap
主要多了对集合中的元素根据键排序的能力以及对集合内元素的搜索的能力
HashSet
、LinkedHashSet
和 TreeSet
都是 Set
接口的实现类,都能保证元素唯一,并且都线程不安全HashSet
、LinkedHashSet
和 TreeSet
的主要区别在于底层数据结构不同。HashSet
的底层数据结构是哈希表(基于 HashMap
实现)。LinkedHashSet
的底层数据结构是数组和双向链表+红黑树,元素的插入和取出顺序满足 FIFO。TreeSet
底层数据结构是红黑树,元素是有序的,排序的方式有自然排序和定制排序。HashSet
用于不需要保证元素插入和取出顺序的场景,LinkedHashSet
用于保证元素的插入和取出顺序满足 FIFO 的场景,TreeSet
用于支持对元素自定义排序规则的场景。ArrayDeque
和 LinkedList
都实现了 Deque
接口,两者都具有队列的功能
ArrayDeque
是基于可变长的数组和双指针来实现,而 LinkedList
则通过链表来实现。ArrayDeque
不支持存储 NULL
数据,但 LinkedList
支持。ArrayDeque
是在 JDK1.6 才被引入的,而LinkedList
早在 JDK1.2 时就已经存在。ArrayDeque
插入时可能存在扩容过程, 不过均摊后的插入操作依然为 O(1)。虽然 LinkedList
不需要扩容,但是每次插入数据时均需要申请新的堆空间,均摊性能相比更慢。从性能的角度上,选用 ArrayDeque
来实现队列要比 LinkedList
更好。此外,ArrayDeque
也可以用于实现栈。
comparable
接口实际上是出自java.lang
包 它有一个 compareTo(Object obj)
方法用来排序
comparator
接口实际上是出自 java.util
包 它有一个compare(Object obj1, Object obj2)
方法用来排序
当你把对象加入HashSet
时,HashSet
会先计算对象的hashcode
值来判断对象加入的位置,同时也会与其他加入的对象的 hashcode
值作比较,如果没有相符的 hashcode
,HashSet
会假设对象没有重复出现。但是如果发现有相同 hashcode
值的对象,这时会调用equals()
方法来检查 hashcode
相等的对象是否真的相同。如果两者相同,HashSet
就不会让加入操作成功。
JDK1.8 之前 HashMap
底层是 数组+链表 。采用的是头插法,先扩容在插入数据,扩容时需要rehash
JDK1.8 之后 HashMap
底层是 数组+链表+红黑树 。采用的是尾插法,先插入数据后扩容,不需要重新计算hash值
HashMap 底层维护了 Node 类型的数组 table,默认为 null
何时扩容
1、数组为空时 即tab = null 或者 tab.length = 0
2、元素个数超过数组长度*负载因子的时候
3、当链表长度大于8且数组长度小于64时
如何扩容
创建时如果没有给定初始容量,默认初始容量为16,每次扩充变为原来2倍。创建时如果给定了初始容量,则扩充为2的幂次方大小。插入元素后如果链表长度大于阈值(默认为8),先判断数组长度是否小于64,如果小于,则扩充数组,反之将链表转化为红黑树,以减少搜索时间。
具体:
HashMap 底层维护了 Node 类型的数组 table,默认为 null,当创建对象时,将加载因子(loadfactor)初始化为 0.75
第1次添加,则需要扩容 table 容量为 16,临界值(threshold)为12 (16*0.75)
以后再扩容,则需要扩容 table 容量为原来的 2 倍(32),临界值为原来的 2倍(32*0.75),即24,依次类推
在 Java8 中,如果一条链表的元素个数超过 TREEIFY_THRESHOLD(默认是8),并且 table 的大小 >= MIN_TREEIFY_CAPACITY(默认64),就会进行树化(红黑树)
为什么扩容是2的次幂
计算元素位置应该是哈希值对数组长度做取余操作( hash % n
)但是 HashMap 通过 (n - 1) & hash
判断当前元素存放的位置。
hash % n == (n - 1) & hash
的前提是 数组长度n 是 2 的次幂。
&
,相对于%
能够提高运算效率,并且能够充分的散列,减少hash碰撞
HashMap 底层维护了 Node 类型的数组 table,默认为 null,当创建对象时,将加载因子(loadfactor)初始化为 0.75
当添加 key-val 时通过 key 的哈希值得到在 table 的索引,然后判断该索引处是否有元素
1、开放地址法:也称为线性探测法,就是从发生冲突的位置开始,按照一定次序(顺延)从hash表找到一个空闲位置,把发生冲突的元素存到这个位置。比如ThreadLocal
2、链地址法:就是把冲突的key,以单向链表来进行存储,比如HashMap
3、再哈希法:使用多个哈希函数,比如布隆过滤器
JDK 1.7 HashMap采用头插法,多线程扩容就会引起链表顺序倒置,形成死循环,数据丢失(用jstack命令定位线程死循环)
JDK 1.8 HashMap采用尾插法,死循环和数据丢失的问题已经解决。但是存在数据覆盖:HashMap在执行put操作时,因为没有加同步锁,多线程put可能会导致数据覆盖
如何解决HashMap线程不安全的问题?
底层数据结构:
JDK1.7 的 ConcurrentHashMap
底层采用 分段的数组+链表 实现,JDK1.8 采用的数据结构跟 HashMap1.8
的结构一样,数组+链表+红黑二叉树。
Hashtable
和 JDK1.8 之前的 HashMap
的底层数据结构类似都是采用 数组+链表 的形式
🌟实现线程安全的方式:
ConcurrentHashMap
对整个桶数组进行了分割分段(Segment
,分段锁),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。ConcurrentHashMap
已经摒弃了 Segment
的概念,而是直接用 Node
数组+链表+红黑树的数据结构来实现,并发控制使用 synchronized
和 CAS
来操作。(JDK1.6 以后 synchronized
锁做了很多优化) 整个看起来就像是优化过且线程安全的 HashMap
Hashtable
(同一把锁) :使用 synchronized
来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈效率越低。Segment
分段锁来保证安全, Segment
是继承自 ReentrantLock
。JDK1.8 放弃了 Segment
分段锁的设计,采用 Node + CAS + synchronized
保证线程安全,锁粒度更细,synchronized
只锁定当前链表或红黑二叉树的首节点。JDk 7
首先将数据分为一段一段( Segment
)的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其他段的数据也能被其他线程访问。Segment
数组的大小默认是 16,也就是说默认可以同时支持 16 个线程并发写。
ConcurrentHashMap
是由 Segment
数组结构和 HashEntry
数组结构组成。
Segment
继承了 ReentrantLock
,所以 Segment
是一种可重入锁,扮演锁的角色。HashEntry
用于存储键值对数据。
一个 Segment
包含一个 HashEntry
数组,每个 HashEntry
是一个链表结构的元素,每个 Segment
守护着一个 HashEntry
数组里的元素,当对 HashEntry
数组的数据进行修改时,必须首先获得对应的 Segment
的锁。也就是说,对同一 Segment
的并发写入会被阻塞,不同 Segment
的写入是可以并发执行的。
JDK1.8
数据结构跟 HashMap
1.8 的结构类似,采用 数组+链表+红黑树。链表长度超过阈值(8),并且数组长度大于64,将链表(寻址时间复杂度为 O(N))转换为红黑树(寻址时间复杂度为 O(log(N)))。
ConcurrentHashMap
取消了 Segment
分段锁,采用 Node + CAS + synchronized
来保证并发安全。
JDK1.8 中,锁粒度更细,synchronized
只锁定当前链表或红黑二叉树的首节点,这样只要 hash 不冲突,就不会产生并发,就不会影响其他 Node 的读写,效率大幅提升。
并且引入了多线程并发扩容的实现,多个线程对原始数组进行分片,每个线程去负责一个分片的数据迁移,提升扩容效率