1.==与equals()的区别
①==与equals()的相同/相似之处
- 这两者都是用于比较2个变量是否“相同”的
②== 与equals()的区别
- ==是基本运算符,适用于所有类型的变量与变量的对比
- equals()是Object类定义的方法,由于Object是Java的基类(所有类的父类),所以,任何对象都可以调用equals()方法实现对比,但是,基本数据类型并不是对象,无法调用该方法实现对比
- ==对比的是变量的值
- 如果是基本数据类型,对比的是字面值
- 如果是引用数据类型,对比的是引用地址
- equals()只是一个方法,到底返回true 或false取决于方法的实现。默认情况下(根据Object的定义),它与==的结果是相同的
③总结
相同之处:都是对比2个数据是否相同;
不同之处:==符号可以对比所有数据,而equals()只能被对象调用;==符号对比变量的值是否相同,所以,基本类型的变量只要字面值相同即返回true,引用类型的变量仅当引用地址相同时才返回true; equals()方法是Object定义的,默认使用==实现对比,所以,当该方法没有被重写时,执行效果与==相同,如果被重写,则取决于重写的代码,以String类为例,在执行equals()将逐一对比字符串中的每个字符,所以,只要2个String对象的字符完全相同,2个String对象使用equals()对比将返回true;
补充说明:由于Java会在编译期处理常量,并且,常量池中的每个常量都是唯一的,所以,当使用字符串常量直接对变量赋值,或使用[-128,127]区间值对Byte/Short / Integer / Long类型的对象赋值时,使用==对比的结果也是true
实际应用原则:在实际编写代码时,对于基本数据类型的变量,必须使用==进行对比,因为基本数据类型的变量不可以调用equals()方法;对于引用数据类型的变量,推荐使用equals()进行对比,并且,在有必要的情况下,重写equals()方法,使之返回结果的规则符合当前编写代码的需求,在重写时,至少保证同一个对象的对比结果为true(即:如果==对比为true时,则equals()对比返回true) 。
2.什么是hashCode
通常,口头描述中的hashCode指的是hashCode()方法,或该方法的返回值。 hashCode()方法是由Object类定义的,所以,在Java中,所有类都有该方法,并且,所有类都可以重写该方法
误区: hashCode就是对象的内存地址
解读:哈希(hash)一般指散列算法,也称之为哈希算法,在Object类的实现中,哈希码(hashCode)是通过哈希算法得到的一个整型结果,本质上与内存地址没有关系
误区产生原因:根据Object类的hashCode()实现,每个对象的hashCode值(理论上)都不同,通常可以用于判断2个变量是否引用同1个对象
hashCode的作用
- Hash容器可以通过hashCode定位需要使用的对象
- 典型的Hash容器: HashSet、HashMap、HashTable、ConcurrentHashMap
- 再次强调: hashCode不是对象的内存地址
- Hash容器通过hashCode来排除2个不相同的对象
- 例如,向HashSet的元素、HashMap的Key等都要求“唯一”,如果即将添加的元素的hashCode与集合中已有的每个元素的hashCode均不同,则可以视为“当前集合中尚不存在即将添加的元素”
- 如果2个对象的hashCode相同,Hash容器还会调用equals()方法,仅当equals()也返回true时,才会视为“相同”
总结
- hashCode()是Object定义的方法,它将返回一个整型值,它并不代表对象在内存中的地址,它存在的价值是为Hash容器处理数据时提供支持,Hash容器可以根据hashCode定位需要使用的对象,也可以根据hashCode来排除2个不相同的对象,即: hashCode不同,则视为2个对象不同
- 在重写hashCode()时,应该遵循Java SE的官方指导︰
- 如果2个对象使用equals()对比的结果为true,则这2个对象的hashCode()返回的结果应该相同
- 如果2个对象使用equals()对比的结果为false,则这2个对象的hashCode()返回的结果应该不同
- 通常,你不必关心如何重写equals()方法和hashCode()方法,而是使用IDE生成,例如Eclipse、IntelliJ IDEA,它们生成的方法是符合以上指导意见的
3.String、StringBuffer和StringBuilder的区别
①String、StringBuffer和StringBuilder的共同点
-
都是用于处理字符串数据的类
-
都是管理内部的一个char[]实现的
-
实现的接口大致相同,特别是CharSequence接口
- 许多API的设计中,方法的参数或返回值都使用这个接口,使得参数或返回值更加灵活
-
有许多相同的API,例如replace()、indexOf()等
String的“不可变”特性是由于其内部通过管理一个char[]决定的。在Java语言中,数组在内存必须是连接的,则其长度不可变 StringBuffer和StringBuilder从一开始就会使用长度更长的char[],哪怕只用于存放少量的几个字符。其length()方法会返回实际存放的字符数量
在许多调整字符串的操作中,StringBuffer和StringBuilder只需要直接调整内部的char[]即可,不需要频繁的寻址、创建新对象等操作,所以,实际执行效率远高于String类! 当然,如果默认的char[]长度(实际长度)不足以满足运算需求时,会自动扩容,也需要创建新的对象
- StringBuffer是线程安全的,而StringBuilder不是
总结
①相同之处: - 都是用于处理字符串数据的类。都是管理内部的一个char[]实现的
- 实现的接口大致相同,特别是CharSequence接口
- 有许多相同的API,例如replace()、indexOf()等
②不同之处: - String的字符串操作效率低下,是因为它的“不可变”特性决定的
- StringBuffer和StringBuiler会保证管理的char[]的长度始终高于实际存入的字符长度,在处理字符串操作时,效率远高于String
- StringBuffer是线程安全的,而StringBuilder不是
③实际使用原则:尽管StringBuffer和StringBuilder在处理字符串时的效率远高于String,但并不是每个String都需要频繁的改变,相比之下,使用String的语法更加简洁、直观,实际占用的存储空间更小,所以,当字符串不需要频繁的改变时,优先使用String。如果字符串需要频繁改变,原则上来说,仅当单线程运行时,或已经采取措施保障线程安全时,优先使用StringBuilder,因为它的执行效率高于StringBuffer,事实上,尽管StringBuilder的执行效率比StringBuffer高,但差距并不天,为了避免后续调整带来的安全隐患,当字符串可能频繁改变时,一般使用StringBuffer。
4.ArrayList与LinkedList的区别
①ArrayList与LinkedList的共同点:
- 都是List接口的实现类
- 都是序列的,可存储相同元素
- 绝大部分情况下,不关心特有方法
关于“序列的”∶
- 在List集合中的各元素都有索引,类似数组下标,是顺序编号的
- 不推荐描述为“有序的”,详见后续LinkedList的存储结构
- 同理,不要将Set集合描述为“无序的”,只能描述为“散列的”,例如TreeSet、LinkedHashSet的各元素就可以表现出“有序”的特征
关于“存储相同元素”∶
- 在使用集合时,仅当2个对象的hashCode()返回值相同,且equals()对比结果为true时,视为“相同” - Set集合不可以存储相同元素
②ArrayList与LinkedList的区别:
- ArrayList的底层实现是基于数组的
- 优点:查询效率高
- 缺点:修改效率低
- LinkedList的底层实现是基于双向链表的
- 内部使用“节点”管理元素
- 优点:修改效率高–删除
- 缺点:查询效率偏低
③错误的表达
- 错误的表达:当需要查询时,使用ArrayList;当需要修改时,使用LinkedList
- 解读:
- 尽管ArrayList易读难写,但是,没有写入数据,则无从读起
- 尽管LinkedList易写难读,但是,光写入,不读取,没有任何意义
- ArrayList和LinkedList这2者之间也没有继承关系,不可互相转换
- 该“错误的表达”是因为描述不精准
- 因为LinkedList的底层实现基于双向链表,当添加元素时,本质是基于新元素创建“节点”,每个节点需要记录指向前一个节点和后一个节点的引用,占用的存储空间更多。
扩展
- 无论是ArrayList,还是LinkedList,都是线程不安全的,当在多线程中需要使用List时,应该使用CopyOnWriteArrayList
总结:
-
相同之处:
- 都是List接口的实现类
- 一都是序列的,可存储相同元素
- 绝大部分情况下,不关心特有方法
- 都是线程不安全的
- 都是List接口的实现类
-
不同之处:
- ArrayList的底层实现是基于数组的,所以,查询效率高,但修改效率偏低
- LinkedList的底层实现是基于双向链表的,所以,查询效率偏低,但修改效率高,另外,其内部本质上管理的是多个节点,每个节点需要记录指向前一个节点和后一个节点的引用,占用的存储空间更多
-
实际使用原则:在使用简单的字符串作为集合元素时,在10万级别的元素数量时,ArrayList和LinkedList的性能差异并不明显(在绝大部分情况下,使用List时的元素数量都不超过100个,尽管元素数据更加复杂),并且,不可以单纯的只读不写,或只写不读,同时,基于ArrayList占用的存储空间更少,一般使用ArrayList即可,仅当需要极致的追求性能时,再根据读写频率来区分使用,但是,当需要考虑线程安全问题时,则使用CopyOnWriteArrayList。
5.什么是volatile
volatile是Java语言中的一个关键字,可以修饰类的属性。其英文释义一般是:不稳定的
public class volatileDemo {
public volatile int i;
}
volatile的主要作用
- volatile的主要作用有:
- 禁止指令重排
- 确保多线程时属性的可见性
关于指令重排
- 指令重排是CPU和编译器决定的,一定程度上人为不可控
- 指令重排的目的是优化指令,提高执行效率,在单线程中,执行结果不会出现问题,但是,在多线程中,可能出现预期外的结果,所以,应该为共享变量添加volatile关键字进行修饰
关于属性的可见性
- 每个线程在执行过程中,有专属的工作内存空间,当需要某个值时,会优先从工作内存中查找,如果工作内存中没有,则会从主内存中将值复制到工作内存中并缓存。
- 在多线程情景下,可能存在:X线程已经将值缓存到工作内存中,Y线程改变了主内存的值,但X线程仍使用工作内存中缓存的值(尚未从主内存中同步最新的值)
关于属性的可见性 - 在多线程中,由于各线程会优先从工作内存中获取共享变量的值,可能导致某线程更新了共享变量的值,但其它线程仍使用工作内存中缓存的值,出现属性可见性问题,添加volatile即可解决此问题
常见的误区
- 误区:使用synchronized后不需要使用volatile
- 解读:两者都是用于解决多线程相关问题的,但问题的情景并不相同,通常,使用synchronized解决的问题大多是“多个线程执行相同的代码”的情景,而使用volatile解决的问题大多是“多个线程执行的代码不同,但使用到了相同的共享变量”的情景。
总结:
- 关于volatile:
- 它是一个关键字,用于修饰类的成员属性
- 它的主要作用有:
- 禁止指令重排
- 确保多线程时属性的可见性
- synchronized和volatile均不可替代彼此,虽然两者都是用于解决多线程相关问题的,但问题的情景并不相同,通常,使用synchronized解决的问题大多是“多个线程执行相同的代码”的情景,而使用volatile解决的问题大多是“多个线程执行的代码不同,但使用到了相同的共享变量”的情景
- 实际使用原则:当某个属性出现在多个方法中,至少有l个方法会改变该属性的值,且这些方法可能同时被不同的线程执行,则应该为属性添加volatile关键字。
6.Thread类中的start()和run()方法的区别
-
关于Thread类的start()方法
- 是用于启动线程的方法
- 其内部会(自动的)调用run()方法
- 通常,每个线程对象只能调用l次该方法
-
关于Thread类的run()方法
- 是在线程启动后(自动的)被调用的方法
- 用于编写子线程执行的代码
- 默认的run()方法会尝试调用Runnable对象(如果存在的话)的run()方法,否则,什么都不执行,也不返回任何值
- 如果你创建线程对象时使用Runnable对象作为构造方法的参数,当线程启动用会调用Runnable对象的run()方法,所以,你应该在Runnable实现类中实现run()方法
- 如果你创建的是Thread子类的对象,则应该在Thread子类中重写run()方法
常见的误区
- 误区:Runnable是线程接口
- 解读:Runnable表示“可执行的”,创建Thread对象时,可以使用Runnable接口类型的对象作为构造方法的参数,并且,在子线程中执行的确实是Runnable实现类中的run()方法,但是,Runnable自身并不是线程接口,事实上,还有许多其它类都可能使用到Runnable,但与线程完全没有关系。
总结:
- 关于start()方法:
- 是用于启动线程的方法
- 其内部会(自动的)调用run()方法
- 通常,每个线程对象只能调用1次该方法
- 关于run()方法:
- 是在线程启动后(自动的)被调用的方法
- 用于编写子线程执行的代码
- 默认的run()方法会尝试调用Runnable对象(如果存在的话)的run()方法,否则,什么都不执行,也不返回任何值
- 使用Runnable接口时,应该实现run()方法
- 使用Thread子类时,应该重写run()方法