Java面试题(一)

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接口的实现类
      • 一都是序列的,可存储相同元素
      • 绝大部分情况下,不关心特有方法
    • 都是线程不安全的
  • 不同之处:

    • 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()方法
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值