1. ArrayList、LinkedList、Vector的区别
答:
集合关系
相同点:
ArraList、LinkedList、Vector 都实现了 List 接口。
不同点:
(1) ArrayList 底层是用数组实现的,可以认为 ArrayList 是一个可改变大小的数组。
当越来越多的元素添加到 ArrayList 的时候,它的大小会动态增长。
由于 ArrayList 本质上是一个数组,所以它的元素可以直接通过 get 和 set 方法来访问。
(2) LinkedList 底层是用双向链表实现的。所以,在添加和删除操作上,性能比 ArrayList 好;在查询和修改操作上,性能比 ArrayList 差。
(3) Vector 和 ArrayList 几乎是一样的,区别在于 Vector 是线程安全的。因此,性能上较 ArrayList 要差。另一个区别是扩容策略不一样:当越来越多的元素添加进来需要更大空间的时候,Vector 的大小增长为原来的两倍,而 ArrayList 的大小增长原来的50%。
总结:
- ArrayList —— 随机访问(get/set)
- LinkedList —— 插入和删除(add/remove)
- Vector —— 线程安全(synchronized)
2. Sring、StringBuffer、StringBuilder的区别
答:
String 字符串常量
StringBuffer 字符串变量(线程安全)
StringBuilder 字符串变量(非线程安全)
String 类型和 StringBuffer 类型的主要性能区别在于:
-
String 是不可变的对象,因此在每次对 String 类型进行改变的时候其实都等同于生成一个新的 String 对象,然后将指针指向新的 String 对象。
所以经常改变内容的字符串最好不要用 String ,因为每次生成对象都会对系统性能产生影响,特别当内存中无引用对象多了以后, JVM 的 GC 就会开始工作,那速度是一定会变慢的。 - 而如果是使用 StringBuffer 类则结果就不一样了,每次结果都会对 StringBuffer 对象本身进行操作,而不是生成新的对象,再改变对象引用。
所以在一般情况下我们推荐使用 StringBuffer ,特别是字符串对象经常改变的情况下。而在某些特别情况下, String 对象的字符串拼接其实是被 JVM 解释成了 StringBuffer 对象的拼接,所以这些时候 String 对象的速度并不会比 StringBuffer 对象慢。
在大部分情况下 StringBuffer > String
StringBuffer
java.lang.StringBuffer 是线程安全的可变字符序列。一个类似于 String 的字符串缓冲区,但不能修改。虽然在任意时间点上它都包含某种特定的字符序列,但通过调用某些方法可以改变该序列的长度和内容。
可将字符串缓冲区安全地用于多个线程。可以在必要时对这些方法进行同步,因此任意特定实例上的所有操作就好像是以串行顺序发生的,该顺序与所涉及的每个线程进行的方法调用顺序一致。
StringBuffer 的主要操作是 append 和 insert 方法,可重载这些方法,以接受任意类型的数据。
每个方法都能有效地将给定的数据转换成字符串,然后将该字符串的字符追加或插入到字符串缓冲区中。
append 方法始终将这些字符添加到缓冲区的末端;而 insert 方法则在指定的点添加字符。
在大部分情况下 StringBuilder > StringBuffer
StringBuilder
java.lang.StringBuilder 一个可变的字符序列是5.0新增的。此类提供一个与 StringBuffer 兼容的 API,但不保证同步。该类被设计用作 StringBuffer 的一个简易替换,用在字符串缓冲区被单个线程使用的时候(这种情况很普遍)。如果可能,建议优先采用该类,因为在大多数实现中,它比 StringBuffer 要快。两者的方法基本相同。
总结:
- 如果要操作少量的数据 —— String
- 单线程操作大量数据 —— StringBuilder
- 多线程操作大量数据 —— StringBuffer
3. HashMap与Hashtable
相同点
HashMap
- HashMap 是基于哈希表实现的,每一个元素是一个 key-value 对,其内部通过单链表解决冲突问题,容量不足(超过了阀值)时,同样会自动增长。
- HashMap 是非线程安全的,只是用于单线程环境下,多线程环境下可以采用 concurrent 并发包下的 ConcurrentHashMap。
- HashMap 实现了 Serializable 接口,因此它支持序列化,实现了Cloneable 接口,能被克隆。
HashMap 存数据的过程是:
HashMap 内部维护了一个存储数据的 Entry 数组,HashMap 采用链表解决冲突,每一个 Entry 本质上是一个单向链表。当准备添加一个 key-value
对时,首先通过 hash(key) 方法计算 hash 值,然后通过
indexFor(hash,length) 求该 key-value 对的存储位置,计算方法是先用
hash&0x7FFFFFFF 后,再对 length 取模,这就保证每一个 key-value 对都能存入 HashMap 中,当计算出的位置相同时,由于存入位置是一个链表,则把这个 key-value 对插入链表头。
Hashtable
- Hashtable 同样是基于哈希表实现的,同样每个元素是一个 key-value
对,其内部也是通过单链表解决冲突问题,容量不足(超过了阀值)时,同样会自动增长。 - Hashtable 是 JDK1.0 引入的类,是线程安全的,能用于多线程环境中。
- Hashtable同样实现了Serializable接口,它支持序列化,实现了Cloneable接口,能被克隆。
HashMap与Hashtable的区别
(1) 继承的父类不同
Hashtable 继承自 Dictionary 类,而 HashMap 继承自 AbstractMap 类。但二者都实现了 Map 接口。
(2) 线程安全性不同
Hashtable 中的方法是 synchronized 的,而 HashMap 中的方法在默认情况下是非 synchronized 的。在多线程并发的环境下,可以直接使用Hashtable,不需要自己为它的方法实现同步,但使用 HashMap 时就必须要自己增加同步处理。
(3) 是否提供contains方法
- HashMap 把 Hashtable 的 contains 方法去掉了,改成 containsValue 和
containsKey,因为 contains 方法容易让人引起误解。 - Hashtable 则保留了 contains,containsValue 和 containsKey 三个方法,其中 contains 和 containsValue 功能相同。
(4) key和value是否允许null值
- Hashtable 中,key 和 value 都不允许出现 null 值。但是如果在 Hashtable 中有类似 put(null,null) 的操作,编译同样可以通过,因为 key 和 value 都是 Object 类型,但运行时会抛出 NullPointerException 异常,这是 JDK 的规范规定的。
- HashMap 中,null 可以作为键,这样的键只有一个;可以有一个或多个键所对应的值为 null。当 get() 方法返回 null 值时,可能是 HashMap 中没有该键,也可能使该键所对应的值为 null。因此,在 HashMap 中不能由 get() 方法来判断 HashMap 中是否存在某个键, 而应该用 containsKey() 方法来判断。
(5) 两个遍历方式的内部实现上不同
Hashtable、HashMap都使用了 Iterator。而由于历史原因,Hashtable 还使用了 Enumeration 的方式 。
(6) hash值不同
哈希值的使用不同,Hashtable 直接使用对象的 hashCode。而 HashMap
重新计算 hash 值。
hashCode 是 jdk 根据对象的地址或者字符串或者数字算出来的 int 类型的数值。
Hashtable 计算 hash 值,直接用 key 的 hashCode(),而 HashMap 重新计算了 key 的 hash 值,Hashtable 在求 hash 值对应的位置索引时,用取模运算,而 HashMap 在求位置索引时,则用与运算,且这里一般先用hash&0x7FFFFFFF 后,再对 length 取模,&0x7FFFFFFF 的目的是为了将负的 hash 值转化为正值,因为 hash 值有可能为负数,而
&0x7FFFFFFF 后,只有符号外改变,而后面的位都不变。
(7) 内部实现使用的数组初始化和扩容方式不同
- HashTable在不指定容量的情况下的默认容量为11,而HashMap为16,Hashtable 不要求底层数组的容量一定要为2的整数次幂,而 HashMap 则要求一定为2的整数次幂。
- Hashtable 扩容时,将容量变为原来的2倍加1,而 HashMap 扩容时,将容量变为原来的2倍。
- Hashtable 和 HashMap 它们两个内部实现方式的数组的初始大小和扩容的方式。HashTable 中 hash 数组默认大小是11,增加的方式是 old*2+1。
4. Java的四种引用
(1) 强引用(StrongReference)
强引用在程序代码之中普遍存在的,比如下面这段代码中的object和str都是强引用:
Object object = new Object();
String str = "hello";
只要某个对象有强引用与之关联,JVM 必定不会回收这个对象,即使在内存不足的情况下,JVM 宁愿抛出 OutOfMemory 错误也不会回收这种对象。
如果想中断强引用和某个对象之间的关联,可以显示地将引用赋值为null,这样一来的话,JVM在合适的时间就会回收该对象。
(2) 软引用(SoftReference)
软引用是用来描述一些有用但并不是必需的对象,在 Java 中用
java.lang.ref.SoftReference 类来表示。对于软引用关联着的对象,只有在内存不足的时候 JVM 才会回收该对象。因此,这一点可以很好地用来解决 OOM 的问题,并且这个特性很适合用来实现缓存:比如网页缓存、图片缓存等。
软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被 JVM 回收,这个软引用就会被加入到与之关联的引用队列中。
(3) 弱引用(WeakReference)
弱引用也是用来描述非必需对象的,当 JVM 进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。在 java 中,用
java.lang.ref.WeakReference 类来表示。
(4) 虚引用(PhantomReference)
虚引用和前面的软引用、弱引用不同,它并不影响对象的生命周期。在
java 中用 java.lang.ref.PhantomReference 类表示。如果一个对象与虚引用关联,则跟没有引用与之关联一样,在任何时候都可能被垃圾回收器回收。
进一步理解软引用和弱引用
对于强引用,我们平时在编写代码时经常会用到。而对于其他三种类型的引用,使用得最多的就是软引用和弱引用,这2种既有相似之处又有区别。
它们都是用来描述非必需对象的,但是被软引用关联的对象只有在内存不足时才会被回收,而被弱引用关联的对象在JVM进行垃圾回收时总会被回收。
针对上面的特性,软引用适合用来进行缓存,当内存不够时能让JVM回收内存,弱引用能用来在回调函数中防止内存泄露。因为回调函数往往是匿名内部类,隐式保存有对外部类的引用,所以如果回调函数是在另一个线程里面被回调,而这时如果需要回收外部类,那么就会内存泄露,因为匿名内部类保存有对外部类的强引用。
5. 关键字synchronized、volatile、transient
(1) synchronized关键字
synchronized 关键字用于多线程访问程序中的共享资源时实现顺序同步访问资源。可以修饰方法或者代码块。而且关键字 synchronized 取得的锁都是对象锁。
注意:
什么叫对象锁呢,就是一个对象产生一把锁,如果多个线程调用一个对象的多个方法,这些方法都被 synchronized 修饰,那么这些线程共同竞争一把锁,最后表现的就是同步顺序执行各个被 synchronized 修饰的方法。
(1.1) 同步方法
使用 synchronized 修饰的方法是同步方法,多个线程调用同一个对象的同步方法时会顺序同步执行。
(1.1.1) synchronized锁重入
当一个线程执行 synchronized 关键字修饰的方法的时候,其他线程是不可以访问该对象中 synchronized 关键字修饰的方法的,因为唯一的一把锁已经被当前线程使用了。但是如果当前线程在 synchronized 方法/块的内部调用本类或者其父类的其他 synchronized 方法/块,是永远可以得到锁的,这就叫做锁重入。
(1.1.2) 出现异常,同步锁自动释放
当线程执行 synchronized 修饰的代码出现异常时,其所持有的锁会自动释放,从而其他线程可以再次争夺锁的使用权而非一直等待造成死锁。
(1.1.3) 同步不具有继承性
如果父类被 synchronized 关键字修饰,那么线程执行父类代码会同步,但是同步并不会继承给其子类,调用子类的方法仍然是异步的。
(1.2) 同步代码块
一般来说,一个方法处理的内容很多,如果 synchronized 修饰以后,其他同步方法就必须等待其执行完毕才可以继续执行,如果该方法需要较长时间处理,这就明显会降低效率,失去了多线程的意义,所以我们可以考虑将同步的范围缩小,即从同步一个方法缩小为同步一段代码块,这就是同步代码块产生的原因。
同步代码块的语法是:
synchronized (this) {}
synchronized (object) {}
synchronized(this )我称之为 this 同步代码块,针对的是当前对象;synchronized(object) 我称之为非 this 同步代码块,针对的是 object 对象。
注意:
不论是同步方法还是同步代码块,实质上都是争夺锁的问题,而锁一定是对象级的,即一个对象只会产生一个锁,所以只要有一个线程在执行
synchronized 修饰的东西(不论是方法还是代码块),那么其他线程都无法访问被 synchronized 修饰的方法或代码块。
但是注意使用非this同步代码块的时候,里面的 object 不要用 String 类型的,因为大家都知道 JVM 具有 String 常量池缓存的功能,所以使用 String 类型可能产生问题。
(1.3) 静态同步synchronized方法和synchronized(class)代码块
关键字 synchronized 还可以应用在 static 静态方法上,或者
synchronized(class) 代码块。如果这样写,产生的是类级别的锁,也就是给 *.java 这个类加锁,而非给某个对象加锁。这就意味着线程执行同一个类的不同对象的静态同步 synchronized 方法和synchronized(class) 代码块时,都会同步执行。
(2) volatile关键字
Java语言提供了一种稍弱的同步机制,即 volatile 变量,用来确保将变量的更新操作通知到其他线程。当把变量声明为 volatile 类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。volatile 变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取 volatile 类型的变量时总会返回最新写入的值。
在访问 volatile 变量时不会执行加锁操作,因此也就不会使执行线程阻塞,因此 volatile 变量是一种比 sychronized 关键字更轻量级的同步机制。
当对非 volatile 变量进行读写的时候,每个线程先从内存拷贝变量到 CPU 缓存中。如果计算机有多个 CPU,每个线程可能在不同的 CPU 上被处理,这意味着每个线程可以拷贝到不同的 CPU cache 中。而声明变量是 volatile 的,JVM 保证了每次读变量都从内存中读,跳过 CPU cache 这一步。
当一个变量定义为 volatile 之后,将具备两种特性:
- 保证此变量对所有的线程的可见性(可见性,是指线程之间的可见性,一个线程修改的状态对另一个线程是可见的。也就是一个线程修改的结果,另一个线程马上就能看到),当一个线程修改了这个变量的值,volatile 保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新。但普通变量做不到这点,普通变量的值在线程间传递均需要通过主内存来完成。
- 禁止指令重排序优化。有 volatile 修饰的变量,赋值后多执行了一个“load addl $0x0, (%esp)”操作,这个操作相当于一个内存屏障(指令重排序时不能把后面的指令重排序到内存屏障之前的位置),只有一个 CPU 访问内存时,并不需要内存屏障;(什么是指令重排序:是指 CPU 采用了允许将多条指令不按程序规定的顺序分开发送给各相应电路单元处理)。
volatile 性能:
volatile 的读性能消耗与普通变量几乎相同,但是写操作稍慢,因为它需要在本地代码中插入许多内存屏障指令来保证处理器不发生乱序执行。
总结:
总体来说,volatile 是并发编程中的一种优化,在某些场景下可以代替
synchronized。但是,volatile 的不能完全取代 synchronized 的位置,只有在一些特殊的场景下,才能适用 volatile。总的来说,必须同时满足下面两个条件才能保证在并发环境的线程安全:
- 对变量的写操作不依赖于当前值
- 该变量没有包含在具有其他变量的不变式中
(3) transient关键字
transient 关键字为我们提供了便利,你只需要实现 Serializable 接口,将不需要序列化的属性前添加关键字 transient,序列化对象的时候,这个属性就不会序列化到指定的目的地中。
小结:
- 一旦变量被 transient 修饰,变量将不再是对象持久化的一部分,该变量内容在序列化后无法获得访问。
- transient 关键字只能修饰变量,而不能修饰方法和类。注意,本地变量是不能被 transient 关键字修饰的。
- 被 transient 关键字修饰的变量不再能被序列化,一个静态变量不管是否被 transient 修饰,均不能被序列化。
6. foreach与for循环效率对比
(1) 对于数组而言,for 和 foreach 遍历效率相差不大。
(2) 对于集合而言
- 对大多数的集合,foreach 比起传统的 for 循环稍有性能优势但差别不大,因为它对索引的边界值只计算一次。而在对多个集合进行嵌套式迭代时优势会更明显。
- 循环ArrayList时,普通for循环比foreach循环花费的时间要少一点;循环LinkList时,普通for循环比foreach循环花费的时间要多很多。当我将循环次数提升到一百万次的时候,循环ArrayList,普通for循环还是比foreach要快一点;但是普通for循环在循环LinkList时,程序直接卡死。
(3) Effective Java 中建议,一般情况下使用 foreach 进行循环,因为其在简洁性和预防 Bug 方面有着传统 for 循环无法比拟的优势,并且没有性能损失。但是除了以下三种情况:
- 过滤:如果需要遍历集合,并删除选定的元素,就需要使用显式的迭代器,以便可以调用它的remove方法。
- 转换:如果需要遍历列表或数组,并取代它部分或者全部的元素值,就需要列表迭代器ListIterator或者数组索引,以便设定元素的值。(如果直接更改它引用对象的值的话,也可以使用Iterator,前提是符合按引用传递的原则,Iterator的元素为基本数据类型就不会按引用传递,或者它们的包装类,因为是不可变类,也不符合要求。)
- 平行迭代:如果需要并行地遍历多个集合,就需要显式的控制迭代器或者索引变量,以便所有迭代器或者索引变量都可以得到同步前移。
建议:
- 需要循环数组结构的数据时,建议使用普通 for 循环,因为 for 循环采用下标访问,对于数组结构的数据来说,采用下标访问比较好。
- 需要循环链表结构的数据时,一定不要使用普通for循环,这种做法很糟糕,数据量大的时候有可能会导致系统崩溃。
原因:foreach 使用的是迭代器
可以下标访问时,使用 for,不能下标访问,需要指针访问时,使用 foreach。
7. Java是按值传递还是按引用传递?
(1) 什么是按值传递
指的是在方法调用时,传递给参数的是值的拷贝。
按值传递的重要特点:传递的是值的拷贝,也就是说传递后就互不相关了。
(2) 什么是按引用传递
指的是在方法调用时,传递给参数的是引用的地址,也就是变量所对应的内存空间的地址。
按引用传递的重要特点:传递的是引用的地址,也就是说传递前和传递后都指向同一个引用(也就是同一个内存空间)。
总结:
一般情况下,在数据做为参数传递的时候,基本数据类型是值传递,引用数据类型是引用传递(地址传递)。
特例:
String对象做为参数传递时,走的依然是引用传递,只不过String这个类比较特殊。 String对象一旦创建,内容不可更改。每一次内容的更改都是重现创建出来的新对象。
网友评论