Java面试题【必知必会】常见基础题(2024)

近期一直在准备面试,所以为了巩固知识,也为了梳理,整理了一些java的基础面试题!同时也希望各位英雄和女侠能够补充!不胜荣幸!!!

名称地址
Java面试题【必知必会】基础(2024)Go-Go-Go
Java面试题【必知必会】常见基础题(2024)Go-Go-Go
Java面试题【必知必会】MySQL常见面试题(2024)Go-Go-Go
Java面试题【必知必会】Spring常见面试题(2024)Go-Go-Go
Java面试题【必知必会】Mybatis常见面试题(2024)Go-Go-Go
Java面试题【必知必会】SpringMVC常见面试题(2024)Go-Go-Go
Java面试题【必知必会】SpringBoot常见面试题(2024)Go-Go-Go
Java面试题【必知必会】SpringCloud常见面试题(2024)Go-Go-Go
Java面试题【必知必会】Redis常见面试题(2024)Go-Go-Go
Java面试题【必知必会】Linux常用命令面试题(2024)Go-Go-Go

1.什么是面向对象,谈谈你对面向对象的理解?

  1. 面向对象是一种思想,简单来说就是将数据和操作数据的方法封装在对象中。举个例子来说比如洗衣机洗衣服。我们通常会把这个拆分成两个对象——人和洗衣机。人需要干的就是:打开洗衣机—放入衣服—放入洗衣液—关闭洗衣机门-按下各种开关,洗衣机则负责:清洗—烘干

  2. 面向对象拥有三大特性其实也可以说四大特性:封装–继承–多态–抽象

  • 相等于封装来说,就是把一切内部信息隐藏起来,对外不透明,只提供最简单的调用。
  • 继承是从已有的一个类得到信息并创建新类的过程,一般我们称这个提供信息的类为父类,得到信息的的类为子类。子类可以扩展自己的信息,按我自己的理解,继承就是一种信息复用,也是信息延申的一个手段。
  • 多态存在的必要三个条件就是继承,方法的重写,父类引用指向子类对象。就是多个子类继承一个父类,但是重新修改了分享信息的内容,然后通过同样的对象引用调用同样的方法,但是得出不同的信息。多态性其实还分为编译时多态性和运行时多态性,方法重载实现的是编译时多态性(也称为前绑定),方法重写实现的是运行时多态性(也成为后绑定)。
  • 抽象:抽象是将一类对象的共同特征总结出来构造类的过程,包括数据抽象和行为抽象两方面,抽象只关注对象的哪些属性和行为,并不关注这此行为的细节是什么

PS:我还在思考抽象算不算特性!!!!有人说仁者见仁,智者见智。可以是三种也可以是四种

2.== 和equals的区别 ======+2

  1. 首先,==是等于比较运算符,而equals是object里面的一个方法
  2. ==对于基本数据类型来说,比较的是值,对于引用数据类型来书,比较的是内存地址
  3. equals默认情况下也是比较内存地址,但是我们一般会重写equals使其变为内容比较,即值比较

3.final 在 java 中有什么作用————被问+1

  1. final修饰的类叫最终类,该类不能被继承
  2. final修饰的方法不能被重写
  3. final修饰的变量叫常量,常量必须初始化,初始化之后的值不能被修改。
package java_interviewtopic_01.stage01;
import java.util.ArrayList;
import java.util.List;
/**
 * 3.final
 */
public class Interview03 {
    // 使用final修饰的静态类变量需要再声明的时候就赋值或者在静态代码块赋值
    final static String i = "I";  // 声明时赋值
//    static {
//        i = "I";   // 在静态代码块中赋值
//    }
    // 使用final修饰的类变量需要再声明的时候就赋值或者在代码块赋值和构造函数的赋值
    final String j = "J";
//    {
//        j = "J";  // 在代码块中赋值
//    }
//    public Interview03(String j) {
//        this.j = j;  // 在构造函数中赋值
//    }
    public static void main(String[] args) {
        // 局部变量可以先声明后赋值,但是赋值之后也不能再更改了
        final String a;
        a = "abc";
        final List<Integer> list = new ArrayList<>();
        list.add(2);
        list.add(3);
        list.add(4);
        list.add(5);   // 可以看出内容可以变
        // list会爆红
        list = new ArrayList<>();   // 可以看出地址不能变了
    }
}

PS:当final修饰的变量是一个基本数据类型的时候,这个变量初始化之后的值不能再被更改。但是当final修饰的是一个引用数据类型的时候,该引用的内存地址不能再更改,但是该地址的内容可以改变
在这里插入图片描述

4.java 中操作字符串都有哪些类?它们之间有什么区别?===========被问+1

  1. String StringBuffer StringBuilder
  2. 三者共同之处:都是final类,不能被继承
  3. StringBuffer与StringBuilder两者共同之处:可以通过append、indert进行字符串的操作。
  4. 再说这三个类的主要区别:其主要区别在两个方面,即运行速度和线程安全两个方面
  • 先说运行速度,在这方面运行速度快慢为:StringBuilder > StringBuffer > String。String慢的原因:因为Stirng为字符常量,而另外两个均为字符串变量。

  • String的课外补充:写个例子:Stirng a = “123” 再将a = a + “45” 这时打印出a = “12345”这个例子看似这个a被更改了。其实不是,这只是一个假象而已。JVM对于这几行代码是这样处理的,首先创建一个String对象a。并把123赋值给a其实在a = a + “45”的时候,JVM又创建了一个新的对象也名为a。然后再把原来的a的值和”45”加起来再赋值给新的a。而原来的a就会被回收机制给回收掉。所以,a实际上并没有被更改,也就是前面说的String对象一旦创建之后不可更改了。

  • Java中对String对象进行的操作实际上是一个不断创建新的对象并且将旧的对象回收的一个过程,所以执行速度很慢。而StringBuilder和StringBuffer的对象是变量,对变量进行操作就是直接对该对象进行更改,而不进行创建和回收的操作,所以速度要比String快很多。

  • 再说这个线程安全:StringBuilder是线程不安全的,而StringBuffer是线程安全的

  • 如果一个StringBuffer对象在字符串缓冲区被多个线程使用时,StringBuffer中很多方法可以带有synchronized关键字,所以可以保证线程是安全的,但StringBuilder的方法则没有该关键字,所以不能保证线程安全,有可能会出现一些错误的操作。所以如果要进行的操作是多线程的,那么就要使用StringBuffer,但是在单线程的情况下,还是建议使用速度比较快的StringBuilder。

PS:总结一下:

  • String:适用于少量的字符串操作的情况
  • StringBuilder:适用于单线程下在字符缓冲区进行大量操作的情况
  • StringBuffer:适用多线程下在字符缓冲区进行大量操作的情况

5.重载(Overload)和重写(Override)的区别。重载的方法能否根据返回类型进行区分?

  1. 区别:方法的重载和重写都是实现多态的方式,区别在于前者实现的是编译时的多态性,而后者实现的是运行时的多态性具体回答:重载发生在同一个类中,方法名一样,但是参数类型不一样,个数不一样,方法返回值和访问修饰符可以不同。重写发生在父子类中,方法名,参数必须相同,返回值范围小于等于父类,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类。

  2. 不能根据返回值类型来区分:这个时候如果出现这种情况:

  • float max ( int a , int b );
  • int max ( int a , int b );
  • 编译器就不知道该调用哪个了。

6.接口和抽象类有什么区别?=======+1

  1. 定义:接口定义了一组方法的契约,而不包含具体的实现代码。它只声明了方法的名称、参数和返回类型,但没有提供方法的实现细节。抽象类是一个可以包含抽象方法的类,它可以定义方法的实现和属性。

  2. 多继承:接口支持多重继承,一个类可以实现多个接口,以便获得不同的行为和能力。抽象类不支持多重继承,一个类只能继承一个抽象类。

  3. 实现:类通过实现接口来表明它们具有特定的行为或能力。一个类可以实现多个接口,并且必须实现接口中声明的所有方法。抽象类可以被继承,并可以提供一些默认的方法实现,子类可以选择性地覆盖或继承这些方法。

  4. 构造函数:接口不能包含构造函数,因为接口只是一组方法的契约。抽象类可以包含构造函数,它可以被子类继承和调用。

  5. 访问修饰符:接口中的方法默认是公共的,不能有访问修饰符。抽象类中的方法可以有不同的访问修饰符,如公共(public)、私有(private)、受保护(protected)等。

  6. 代码复用:通过实现接口,一个类可以重用多个接口中定义的方法。抽象类可以通过被继承来实现代码的复用。

PS:总的来说,接口主要用于定义契约和行为规范,而抽象类主要用于提供一些默认的方法实现和属性,
以及为子类提供代码复用的机制。选择使用接口还是抽象类取决于具体的设计需求和代码结构。

7.List、Set之间的区别是什么?

  1. 重复元素:List允许包含重复的元素,而Set不允许重复元素。当向List中添加元素时,无论元素是否已经存在,
    都会被添加到List中。而向Set中添加元素时,如果元素已经存在于Set中,则添加操作将被忽略。

  2. 顺序性:List是有序的集合,它维护元素的插入顺序。元素在List中的位置是由插入顺序决定的,
    可以通过索引访问和操作元素。Set是无序的集合,它不保留元素的插入顺序。元素在Set中的存储位置由Set的实现决定,
    无法通过索引访问元素。

  3. 数据结构:List通常使用动态数组(如ArrayList)或链表(如LinkedList)实现,
    这些数据结构提供了快速的随机访问或插入/删除操作。Set通常使用哈希表(如HashSet)或平衡二叉树(如TreeSet)实现,
    这些数据结构提供了高效的元素查找和去重操作。

  4. 主要操作:List提供了通过索引访问元素、在指定位置插入/删除元素、获取列表大小等操作。
    Set提供了添加元素、删除元素、判断元素是否存在等主要操作。

  5. 迭代顺序:List可以通过迭代器按顺序访问集合中的元素。Set的迭代顺序是不确定的,取决于底层数据结构的实现方式。

PS:需要根据具体的需求选择使用List还是Set。如果需要保留元素的插入顺序并且允许重复元素,应该选择List。如果需要高效地进行元素去重和判断元素是否存在的操作,可以选择Set。

8.两个对象的 hashCode()相同,则 equals()也一定为 true,对吗?

  1. 不对

  2. 因为hashcode方法一般用作于哈希表数据结构,在集合中要存入一个元素的时候,首先调用hashcode方法得出hash值,然后将hash值转换为数组下标,然后拿着该下标去对应的位置,如果该位置没有任何的元素,那么就直接将值存入。如果已经存在改值,那么则进行equals比较,如果返回为false,那么将其存入进去,如果返回为true,则覆盖元素。

  3. 结论:hashCode()相等的两个对象他们的equal()不一定相等,也就是hashCode()不是绝对可靠的。equals相等的两个对象它们的hashcode一定相等,也就是equals对比是绝对可靠的。

PS:hashCode()相等即两个键值对的哈希值相等,然而哈希值相等。并不一定能得出键值对相等

9.java 中的 Math.round(-5.5) 等于多少?

等于-5,在java中,四舍五入的原理是在原参数上加0.5再做向下取整。

10.String str=”i”与 String str=new String(“i”)一样吗?

  1. 不一样,两个内存分配的方式不一样
  2. String str = “i” 的方式java会把它分配到常量池
  3. String str = new String(“i”) 会分配到堆内存中

11. String s = new String(“xyz”);创建了几个String对象?

这个要看情况,当常量池中没有”xyz”的时候,他会先在常量池中创建这个字符串对象,然后再创建这个字符串的引用对象,所以这时候是两个对象。当常量池中有这个”xyz”的时候,就只会创建这个字符串的引用对象,即一个对象

12. ArrayList 和 LinkedList 的区别是什么?————被问+1

  1. 内部实现:
  • ArrayList内部使用一个动态数组来存储元素。它可以根据需要自动调整数组的大小,支持随机访问和快速的索引操作。
    插入和删除元素时,可能需要移动后续元素来填补空缺或调整数组大小。
  • LinkedList内部使用一个双向链表来存储元素。每个节点都包含元素本身和指向前一个和后一个节点的引用。
    它支持快速的插入和删除操作,但访问特定索引的元素需要从头部或尾部开始遍历链表。
  1. 访问操作:

-. ArrayList支持快速的随机访问,可以通过索引直接访问和修改元素。时间复杂度为O(1)。
-. LinkedList的访问操作需要从头部或尾部开始遍历链表,直到达到目标索引。访问特定索引的元素需要遍历链表,
时间复杂度为O(n)。

  1. 插入和删除操作:
  • ArrayList在末尾进行插入和删除操作效率较高,时间复杂度为O(1)。但在中间位置进行插入和删除操作需要移动后续元素,
    时间复杂度为O(n)。
  • LinkedList在任意位置进行插入和删除操作效率较高,只需要调整相邻节点的引用,时间复杂度为O(1)。
    但在特定位置的访问操作效率较低,时间复杂度为O(n)。
  1. 内存占用:
  • ArrayList在内存中连续存储元素,因此它的内存占用相对较小。
  • LinkedList的每个节点都需要额外的空间来存储前后节点的引用,因此它的内存占用相对较大。

PS:综上所述,ArrayList适合需要频繁访问和修改元素的场景,而LinkedList适合需要频繁插入和删除元素的场景。选择哪个实现类取决于具体的需求和操作模式。

13. HashMap 的实现原理?

  1. 哈希函数:HashMap 使用哈希函数将键映射到哈希表的索引位置。哈希函数通常根据键的哈希码计算,哈希码是一个整数值,
    用于表示键的特征。Java 中的对象通过 hashCode() 方法获取哈希码。

  2. 数组和链表/红黑树:HashMap 内部使用数组存储元素。数组的每个位置称为桶(bucket),
    每个桶可以存储一个链表或红黑树的根节点。当多个键映射到同一个桶时,它们将以链表或红黑树的形式存储在该桶中。

  3. 冲突处理:由于哈希函数的映射范围可能小于键的数量,不同的键可能会映射到相同的桶中,这称为哈希冲突。
    当发生冲突时,HashMap 使用链表或红黑树来解决。初始情况下,所有键都存储在链表中。

  4. 插入操作:当执行插入操作时,首先根据键的哈希码计算桶的索引位置。如果桶为空,则直接将键值对存储在该桶中。
    如果桶非空,表示存在冲突,则遍历桶中的链表或红黑树,根据键的哈希码和 equals() 方法判断键是否已经存在。
    如果键已存在,则更新对应的值;如果键不存在,则将新的键值对插入到链表或红黑树的末尾。

  5. 查找操作:执行查找操作时,根据键的哈希码计算桶的索引位置。如果桶为空,则键不存在;如果桶非空,
    则遍历链表或红黑树,根据键的哈希码和 equals() 方法判断键是否匹配。如果找到匹配的键,则返回对应的值;
    如果遍历结束仍未找到匹配的键,则键不存在。

  6. 删除操作:执行删除操作时,根据键的哈希码计算桶的索引位置。如果桶为空,则键不存在;如果桶非空,
    则遍历链表或红黑树,根据键的哈希码和 equals() 方法判断键是否匹配。如果找到匹配的键,
    则将对应的节点从链表或红黑树中删除

PS:hashMap的put原理: hashMap的底层采用数组和链表以及红黑树(jdk1.8)的数据结构。当我们往HashMap里面put元素的时候,底层调用k的hashcode方法得到hash值,然后通过哈希算法将hash值转换为数组的下标,当下标上没有任何元素的时候,就把这个节点放在这个位置上,如果下标对应的位置上有链表,此时会拿着k和链表上的k进行equals比较,如果和所有链表上的k进行equals比较都是返回false,则将其存入末尾(1.7是头部),如果有一个返回为true,则将它的值覆盖,如果添加时发现容量不够,就开始扩容。

补充点:在jdk8版本的时候haspMap在第一次添加数据时,默认构造函数构建的初始容量是16,当达到它的临界值(0.75)的时候,数组就会扩容,如果有一条链表的元素个数到达8,且数组的大小到达64时。就会进化成红黑树,当数组的长度重新低于6的时候,又会将红黑树重新转换为链表。

PS:hashMap的get(k)原理:先调用k的hashcode方法得出hash值,通过哈希算法将hash值转换为数组下标,通过数组下标快速定位,如果该位置上什么都没有,则返回null。如果该位置有链表,那么拿着该k和链表上的所有k进行equals比较, 如果所有equals都返回false则返回null,但是只要其中一个节点返回true,则将其value返回。

14. 说一下 HashSet 的实现原理?

  1. HashSet底层由HashMap实现,扩容机制一样。它封装了一个HashMap来存储所有的集合元素。但是它不一样的是,它的所有集合元素由HashMap的key来保存,而HashMap的value则存储了一个PRESENT。它是一个静态的 Object 对象。
  2. HashSet的其它操作原理都是基于HashMap的

15. HashMap 和 Hashtable 有什么区别?

  1. 线程安全性:Hashtable是线程安全的,它的方法都是同步的,可以在多线程环境下使用。而HashMap则是非线程安全的,
    它的方法没有进行同步处理,如果在多线程环境下使用,需要手动进行同步操作。

  2. null值:Hashtable不允许键或值为null,如果尝试存储null键或值,将会抛出NullPointerException。
    而HashMap允许键和值都为null,可以存储null键和null值。

  3. 继承关系:Hashtable是早期Java版本中提供的类,它是Dictionary类的子类。
    而HashMap是Java Collections Framework中的一部分,它是AbstractMap类的子类。

  4. 迭代器:Hashtable的迭代器是通过Enumeration实现的,而HashMap的迭代器是通过Iterator实现的。
    Iterator提供了更强大的迭代功能,可以同时进行遍历和删除操作。

  5. 初始容量和扩容机制:Hashtable初始化容量是11,扩容是2n+1,hashmap初始化容量是16,扩容是2n

  6. 性能:由于Hashtable是线程安全的,它的方法都进行了同步处理,这可能会导致在性能方面的一些开销。相比之下,HashMap不进行同步操作,因此在单线程环境下通常具有更好的性能。

PS: 综上所述,如果在多线程环境下需要线程安全的操作,可以选择Hashtable。而在单线程环境下,或者需要更高的性能和灵活性,可以选择HashMap。

16. HashSet与HashMap的区别?

  1. 存储结构:HshMap存储键值对,HashSet存储对象

  2. 存储内容:HashSet不允许重复的元素。HashMap每个键都是唯一的,但值可以重复。

  3. 使用方式:HashMap使用put添加元素值,HshSet使用add方法

  4. 实现接口:HashMap实现了Map接口,HashSet实现了set接口

  5. 存储效率:HashSet 的存储效率比较高,因为它只需要存储单个元素。HashMap 的存储效率相对较低,
    因为它需要存储键值对,并且需要处理键的哈希冲突。

  6. 计算hashCode的方式不同:HashMap使用键(key)来计算hashCode, HashSet使用成员对象来计算hashcode的值,对于两个对象来说,他们的hashcode值可能相同,所以用equals()方法来判断对象的相等性。如果两个对象不相等的话返回false

PS:需要注意的是,HashSet 实际上是通过 HashMap 来实现的,它使用 HashMap 的键作为元素的存储和查找依据,值则统一为一个常量对象。因此,HashSet 的实现可以看作是对 HashMap 的简化和特化。

17. &和&&的区别?======+1

  1. &和&&都表示与的意思,既表达式俩边都成立,结果才成立
  2. &做逻辑运算符时,左边为假时,它还会计算右边,而&&(短路与)不会,当左边为假时后面则不会计算了
  3. &做位运算符时,&的左右俩边可以是布尔类型,也可以是数值,而&&只能是布尔类型

18. 字符串连接用+和StringBuilder的append的区别?

  1. 一般情况下没什么区别,因为一般情况下用+连接,系统内部会进行优化,它用的也是StringBuilder的append来实现的
  2. 但是在循环拼接的时候,用的如果是+拼接的话,就是一直在循环内部创建StringBuilder对象,这样会造成空间浪费。但是我们直接用StringBuilder的话,可以定义在循环外面。减少内存消耗
  // 建议使用
     StringBuilder stringBuilder = new StringBuilder("123");
     for (int j = 0; j < 10; j++) {
         // 这里使用的是外部创建的StringBuilder,就只用一个
         stringBuilder.append(j);
     }
     // 不建议使用
     String a = "tt";
     for (int j = 0; j < 10; j++) {
         // 这里会一直创建StringBuilder,然后进行append。
         a += j;
     }

19. String有哪些特性?

  1. 不可变,为什么不可变可以查看第四题的课外补充
  2. 常量池优化:String 对象创建之后,会在字符串常量池中进行缓存,如果下次创建同样的对象时,会直接返回缓存的引用
  3. 使用 final 来定义 String 类,表示 String 类不能被继承,提高了系统的安全性

20. Integer a= 127 与 Integer b = 127相等吗?那Integer a1 = 128 与 Integer b1 = 128 呢?

  1. 前者相等,后者不相等
  2. 因为如果整型字面量的值在-128到127之间,那么自动装箱时不会new新的Integer对象,而是直接引用常量池中的Integer对象,超过这个范围 则会new 新的Integer对象,即a1==b1的结果是比较内存地址,所以不相等

21.this关键字的用法?

  1. 引用当前对象:this 可以用于引用当前对象,在类的方法中可以通过 this 来访问当前对象的成员变量和方法。
    例如,this.name 表示当前对象的 name 成员变量,this.method() 表示调用当前对象的 method() 方法。

  2. 区分同名的成员变量和方法:当成员变量和方法名称相同,且需要在方法内部访问成员变量时,
    可以使用 this 关键字来明确指定是访问成员变量还是方法。例如,this.name 表示当前对象的成员变量 name,
    而 name() 表示调用当前对象的 name() 方法。

  3. 在构造方法中调用其他构造方法:在一个类中可以定义多个构造方法,而且一个构造方法可以调用同一个类中的其他构造方法。
    使用 this 关键字可以在构造方法中调用其他构造方法。例如,this(parameters) 可以调用具有相应参数的其他构造方法。

  4. 返回当前对象:在方法中,如果需要返回当前对象本身,可以使用 return this 语句。这在实现方法链式调用时很常见。

PS: 总结起来,this 关键字在Java中用于引用当前对象,区分同名的成员变量和方法,调用其他构造方法以及返回当前对象本身。

22.static存在的主要意义?

  1. 共享数据:静态成员属于类,而不是类的实例。静态成员在类加载时被初始化,并且在整个程序执行期间只有一份拷贝。因此,静态成员可以被多个对象共享,可以在不创建对象的情况下访问和修改。

  2. 节省内存:由于静态成员只有一份拷贝,不需要每个对象都创建一份,可以节省内存空间。对于大量对象共享相同数据的情况,使用静态成员可以降低内存开销。

  3. 方便访问:静态成员可以通过类名直接访问,而不需要通过对象引用。这样可以简化访问代码,提高代码的可读性和易用性。

  4. 静态代码块:static关键字还有一个比较关键的作用就是 用来形成静态代码块以优化程序性能,static块可以置于类中的任何地方,类中可以有多个static块。在类初次被加载的时候会按照static块的顺序来执行每个static块,并且只会执行一次

PS:需要注意的是,静态成员有一些限制和注意事项:

  • 静态成员只能访问其他静态成员,不能直接访问非静态成员。
  • 静态成员不能引用 this 关键字,因为它们不依赖于实例。
  • 静态成员的生命周期与类的生命周期相同,它们在类被加载时初始化,直到程序结束或类被卸载才被销毁。

PS:总结起来,static关键字的主要意义是共享数据、节省内存、方便访问,以及定义静态块和静态方法。

23.为什么说static块可以用来优化程序性能?

  1. 静态块的执行时机:静态块在类加载时执行,而不是在对象实例化时执行。这意味着静态块的初始化操作只会执行一次,并且在程序的整个生命周期内保持有效。这可以避免重复的初始化操作,提高程序的性能。

  2. 提前初始化:静态块可以在类加载时进行一些提前的初始化操作,例如读取配置文件、建立连接等。
    通过在静态块中执行这些操作,可以避免在实际使用时再进行初始化,从而减少了延迟和初始化过程的开销,提高了程序的响应速度。

  3. 资源预加载:静态块可以用来预加载一些资源,例如将数据加载到缓存中,提前准备好一些常用的计算结果等。这样可以避免每次需要资源时都进行加载和计算,从而减少了运行时的开销,提高了程序的执行效率。

PS:总结起来,通过在静态块中进行提前初始化、资源预加载等操作,可以避免重复的初始化和延迟操作,从而提高程序的性能和响应速度。静态块在适当的情况下可以作为一种性能优化的手段之一。

PS(简答):因为它的特性:只会在类加载的时候执行一次。因此,很多时候会将一些只需要进行一次的初始化操作都放在static代码块中进行

24.创建一个对象用什么关键字?对象实例与对象引用有何不同?

1.创建对象使用关键字 “new”

  1. 对象实例是指类的具体实体,它占用内存并在运行时存在。当你使用 “new” 关键字创建一个对象时,系统将分配内存来存储该
    对象的实例。(对象实例在堆内存中)

  2. 对象引用是指访问对象的变量或标识符。它们实际上只是存储对象内存地址的变量。当你创建一个对象时,实际上创建的是对象
    的实例,而引用变量只是指向该实例的一个引用。你可以使用引用变量来操作和访问对象的属性和方法。(对象引用存放在栈内存中)

PS:简而言之,对象实例是指对象在内存中的实体,而对象引用是指用于访问对象的变量。你可以通过对象引用来操作对象,
包括调用对象的方法和访问其属性

25.在Java中定义一个不做事且没有参数的构造方法的作用?—-即空构造方法

Java程序在执行子类的构造方法之前,如果没有用super()来调用父类特定的构造方法,则会调用父类中“没有参数的构造方法”。因此,如果父类中只定义了有参数的构造方法,而在子类的构造方法中又没有用super()来调用父类中特定的构造方法,则编译时将发生错误,因为Java程序在父类中找不到没有参数的构造方法可供执行。解决办法是在父类里加上一个不做事且没有参数的构造方法

26.一个类的构造方法的作用是什么?若一个类没有声明构造方法,该程序能正确执行吗?为什么?

  1. 主要作用是完成对类对象的初始化工作
  2. 可以执行
  3. 因为一个类即使没有声明构造方法也会有默认的不带参数的构造方法

27.静态变量和实例变量区别?======+1

  1. 存储位置和创建节点:
  • 实例变量:每个类的实例都有自己的实例变量副本,它们存储在对象的堆内存中。每当创建一个类的新实例时,
    就会分配实例变量的新副本。
  • 静态变量:静态变量在类的加载过程中被分配内存,并且在整个程序运行期间只有一个副本。它们存储在静态存储区域,
    通常是在静态数据区域。
  1. 生命周期:
  • 实例变量:实例变量的生命周期与对象的生命周期相同。当对象被创建时,实例变量被初始化,而当对象被销毁时,
    实例变量也会随之销毁。
  • 静态变量:静态变量在程序开始运行时初始化,并在整个程序执行期间保持不变,直到程序终止。它们不依赖于任何特定的实例。
  1. 访问方式:
  • 实例变量:实例变量只能通过对类的实例进行引用来访问。每个实例都有自己的实例变量副本,
    因此可以在每个实例上进行不同的赋值和访问。
  • 静态变量:静态变量可以通过类名直接访问,不需要创建类的实例。所有实例共享同一个静态变量副本。
  1. 使用场景:
  • 实例变量:实例变量适用于每个对象具有不同状态或属性的情况。它们在类的实例之间具有独立性,并且可以根据对象的特定
    需求进行不同的赋值。
  • 静态变量:静态变量适用于在多个对象之间共享状态或属性的情况。它们在类的所有实例之间是共享的,可以在不创建
    类的实例的情况下访问和修改。

PS:实例变量是每个对象独立拥有的,每个对象都有自己的一份副本,而静态变量在类加载时初始化,整个程序运行期间只有一份副本,
可以被所有对象共享。

28.静态方法和实例方法有何不同?

  1. 内存分配:
  • 实例方法:每个类的实例都有自己的一份实例方法的副本,分配在堆内存中
  • 静态方法:在类的加载过程中被分配内存,并在整个程序运行期间只有一份副本,分配在静态存储区域。
  1. 访问方式:
  • 实例方法:实例方法必须通过类的实例来调用。在实例方法内部,可以使用关键字 “this” 引用当前对象。
  • 静态方法:静态方法可以直接通过类名来调用,而不需要创建类的实例。它们不能使用关键字 “this”,因为静态方法不依赖于特定的对象实例。
  1. 访问权限
  • 静态方法在访问本类的成员时,只允许访问静态成员(即静态成员变量和静态方法),而不允许直接访问实例成员变量和实例方法;实例方法则无此限制
  1. 继承和重写:
  • 实例方法:实例方法可以被子类继承和重写。当子类定义与父类相同的实例方法时,可以使用方法重写来改变方法的行为。
  • 静态方法:静态方法不能被子类重写。子类可以定义与父类相同的静态方法,但是这只是方法的隐藏,而不是重写。

29.对象的相等与指向他们的引用相等,两者有什么不同?

对象的相等 比的是内存中存放的内容是否相等而 引用相等 比较的是他们指向的内存地址是否相等

30.java 中 IO 流分为几种?

  1. 按功能来分:输入流(input)、输出流(output)

  2. 按类型来分:字节流和字符流

  • 字节流和字符流的区别是:字节流按 8 位传输以字节为单位输入输出数据,字节流用于处理二进制数据
    字节流: InputStream,OutputStream
  • 字符流按 16 位传输以字符为单位输入输出数据,字符流用于处理文本数据
    字符流: Reader,Writer

31.Java中的I/O流分为哪些层次

  1. 字节流抽象类层次:
  • InputStream:字节输入流的抽象类,用于从数据源读取字节数据。
  • OutputStream:字节输出流的抽象类,用于向目标写入字节数据。
  1. 字符流抽象类层次:
  • Reader:字符输入流的抽象类,用于从数据源读取字符数据。
  • Writer:字符输出流的抽象类,用于向目标写入字符数据。
  1. 字节流与字符流桥接层次:
  • InputStreamReader:将字节流转换为字符流的桥接类。
  • OutputStreamWriter:将字符流转换为字节流的桥接类。
  1. 高级I/O流层次:
    在字节流和字符流的基础上,Java还提供了一些高级I/O流,用于更方便地进行数据处理,如:
  • BufferedInputStream 和 BufferedOutputStream:提供缓冲功能,加快读写速度。
  • BufferedReader 和 BufferedWriter:提供缓冲功能和一次读取一行的能力。
  • DataInputStream 和 DataOutputStream:用于读写Java基本数据类型和字符串。
  • ObjectInputStream 和 ObjectOutputStream:用于读写Java对象。

这些层次的结构使得Java中的I/O操作更加灵活,可以根据不同的需求选择合适的类进行数据的读取和写入。

32.什么是处理流(Filter Streams)?什么是节点流(Node Streams)?

  1. 处理流:处理流是对节点流的包装,提供了额外的功能。它们可以提供缓冲、数据转换、对象序列化等功能,以简化I/O操作。

  2. 节点流:节点流是直接与数据源或目标连接的I/O流。它们用于读取或写入数据,例如文件流(FileInputStream、FileOutputStream)和网络流(SocketInputStream、SocketOutputStream)

33.Java中常用的I/O流类有哪些?

Java中常用的I/O流类包括FileInputStream、FileOutputStream、BufferedInputStream、BufferedOutputStream、FileReader、FileWriter、BufferedReader、BufferedWriter等

34.如何使用Java中的序列化(Serialization)?

序列化是将对象转换为字节流的过程,以便在网络传输或持久化存储中使用。要实现序列化,需要让类实现Serializable接口,并使用ObjectInputStream和ObjectOutputStream进行读写操作。

35.如何处理大文件读写的效率问题?

可以使用缓冲流(如BufferedInputStream、BufferedOutputStream)来减少磁盘读写次数,从而提高效率。另外,可以使用RandomAccessFile类进行随机访问大文件。

36.什么是字节缓冲流(Buffered Streams)?什么是字符缓冲流(Buffered Streams)?

  1. 字节缓冲流是对字节流的包装,提供了缓冲功能,可以提高读写的效率。常见的字节缓冲流类有BufferedInputStream和BufferedOutputStream。

  2. 字符缓冲流是对字符流的包装,提供了缓冲功能,可以提高读写的效率。常见的字符缓冲流类有BufferedReader和BufferedWriter。

37.请说说什么是数据流,什么是对象流?

  1. 数据流:数据流是用于读写基本数据类型和字符串的流。DataInputStream和DataOutputStream类提供了读写基本类型数据的方法。

  2. 对象流:是用于读写Java对象的流。ObjectInputStream和ObjectOutputStream类提供了读写对象的方法,并支持对象的序列化和反序列化。

38.什么是标准输入输出流?

标准输入输出流是Java程序默认提供的输入输出流。System.in表示标准输入流(键盘输入),System.out表示标准输出流(控制台输出)。

39. 什么是NIO?

NIO是Java的新I/O库,提供了一种更高效、非阻塞的I/O操作方式。NIO的核心概念包括通道(Channel)和缓冲区(Buffer)。

40.BIO、NIO、AIO 有什么区别?

  1. BIO(同步并阻塞):线程发起IO请求,不管内核是否准备好IO操作,从发起请求起,线程一直阻塞,直到操作完成
  2. NIO(同步非阻塞):线程发起IO请求,立即返回;内核在做好IO操作的准备之后,通过调用注册的回调函数通知线程做IO操作,线程开始阻塞,直到操作完成
  3. AIO(异步非阻塞):线程发起IO请求,立即返回;内存做好IO操作的准备之后,做IO操作,直到操作完成或者失败, 通过调用注册的回调函数通知线程做IO操作完成或者失败
  4. BIO是一个连接一个线程。NIO是一个请求一个线程。AIO是一个有效请求一个线程

41.Java中如何读写文件?

可以使用FileInputStream和FileOutputStream读写字节数据,使用FileReader和FileWriter读写字符数据。也可以使用缓冲流和处理流来提高读写效率和功能

42.什么是字符流和字节流的转换?

字符流和字节流可以通过使用InputStreamReader和OutputStreamWriter类来进行相互转换。InputStreamReader将字节流转换为字符流,而OutputStreamWriter将字符流转换为字节流。

43.如何处理大数据量的IO操作?

对于大数据量的IO操作,可以使用NIO(New I/O)来实现非阻塞IO,使用缓冲区(Buffer)来减少IO次数,以及使用多线程或异步IO来提高并发性能。

44.什么是缓冲区(Buffer)?

缓冲区是一块内存区域,用于在Java I/O操作中存储数据。它提供了一种临时存储数据的机制,可以减少与底层设备之间的频繁交互,从而提高性能。

45.什么是文件过滤器(File Filter)?

文件过滤器是用于过滤文件的一种机制。它可以帮助我们筛选出特定类型或满足特定条件的文件。在Java中,可以使用FileFilter接口或FilenameFilter接口来实现文件过滤器。

46.什么是反射机制?======+1

JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法,对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制

简单地说:将类的各个组成部分封装为其他对象

47.反射机制优缺点?

  • 优点: 运行期类型的判断,动态加载类,提高代码灵活度。可以解耦,提高程序的可扩展性
  • 缺点: 性能瓶颈:反射相当于一系列解释操作,通知 JVM 要做的事情, 性能比直接的java代码要慢很多

48.反射机制的应用场景有哪些?

  1. JDBC连接数据库时使用Class.forName()通过反射加载数据库的驱动程序
  2. Spring框架也用到很多反射机制,Spring 通过 XML 配置模式装载 Bean 的过程:
  • 将程序内所有 XML 或 Properties 配置文件加载入内存中
  • Java类里面解析xml或properties里面的内容,得到对应实体类的字节码字符串以及相关的属性信息
  • 使用反射机制,根据这个字符串获得某个类的Class实例
  • 动态配置实例的属性
  1. 访问私有方法和字段

49.Java获取反射(class)的三种方法

  1. Class.forName(“全类名”):将字节码文件加载进内存,返回class对像
  2. 类名.class:通过类名的属性class获取
  3. 对象.getClass():getClass()方法在Object中定义

50.如何创建对象的实例?

可以通过反射的 newInstance() 方法创建对象的实例。该方法会调用类的默认构造函数来创建对象。

PS:代码演示

package com.shisan.test01;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.Objects;
public class 反射 {
    public static void main(String[] args) {
        try {
            // 获取 Class 对象
            Class<?> clazz = Class.forName("com.shisan.test01.PersonTest");
            Class<?> clazzTest = Class.forName("com.shisan.test01.Test");
            // 获取带参构造方法(此处假设Person类有一个带两个参数的构造方法)
            Constructor<?> constructor = clazz.getConstructor(String.class, int.class);
            Constructor<?> constructorTest = clazzTest.getConstructor(String.class, String.class);
            // 使用构造方法创建对象实例
            Object obj = constructor.newInstance("John", 30);
            Object objTest = constructorTest.newInstance("John", "30");
            // 对象创建成功后,可以将其强制转换为对应的类类型
            PersonTest person = (PersonTest) obj;
            Test objTest1 = (Test) objTest;
            System.out.println("Object created: " + person);
            System.out.println("Object created: " + objTest1);
        } catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException | InstantiationException  | InvocationTargetException e) {
            e.printStackTrace();
        }
    }
}
class Test {
    String name;
    String age;
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public String getAge() {
        return age;
    }
    public void setAge(String age) {
        this.age = age;
    }
    public Test(String name, String age) {
        this.name = name;
        this.age = age;
    }
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Test test = (Test) o;
        return Objects.equals(name, test.name) &&
                Objects.equals(age, test.age);
    }
    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }
    @Override
    public String toString() {
        return "Test{" +
                "name='" + name + '\'' +
                ", age='" + age + '\'' +
                '}';
    }
}
class PersonTest {
    private String name;
    private int age;
    public PersonTest(String name, int age) {
        this.name = name;
        this.age = age;
    }
    @Override
    public String toString() {
        return "Person [name=" + name + ", age=" + age + "]";
    }
}

51.如何调用对象的方法?

可以使用反射的 getMethod() 方法获取方法对象,并使用 invoke() 方法调用方法。需要提供方法名和参数类型来获取对应的方法对象。

PS:代码演示

package com.shisan.反射测试;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class MyClass {
    public static void main(String[] args) {
        try {
            // 获取 Class 对象
            Class<?> clazz = Class.forName("com.shisan.反射测试.Person");
            // 获取方法名称和参数类型
            String methodName = "sayHello";
            Class<?>[] parameterTypes = new Class<?>[] { String.class, int.class };
            // 获取方法对象
            Method method = clazz.getMethod(methodName, parameterTypes);
            // 创建对象实例(假设Person类有一个带两个参数的构造方法)
            Object obj = clazz.getConstructor(String.class, int.class).newInstance("John", 30);
            // 调用方法(此处传入对应的参数值)
            Object result = method.invoke(obj, "Hello, Reflection!", 5);
            System.out.println("Method result: " + result);
        } catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException | InstantiationException | InvocationTargetException e) {
            e.printStackTrace();
        }
    }
}
class Person {
    private String name;
    private int age;
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
    public String sayHello(String message, int repeat) {
        String greeting = "Hello, I'm " + name + ". " + message;
        StringBuilder sb = new StringBuilder(greeting);
        for (int i = 1; i < repeat; i++) {
            sb.append(" ").append(message);
        }
        return sb.toString();
    }
}

52.如何获取和设置对象的字段值?

  1. 可以使用反射的 getField() 方法获取字段对象,并使用 get() 方法获取字段的值。如果字段是私有的,可以使用 getDeclaredField() 方法获取字段对象,并通过 setAccessible(true) 设置字段的可访问性。

  2. 私有字段设置字段值可以使用set方法、例如 通过获取到的字段对象.set(对象实例, 需要设置的字段值)

PS:代码例子

import java.lang.reflect.Field;
public class MyClass {
    public static void main(String[] args) {
        try {
            // 获取 Class 对象
            Class<?> clazz = Class.forName("com.example.Person");
            // 创建对象实例(假设Person类有一个带两个参数的构造方法)
            Object obj = clazz.getConstructor(String.class, int.class).newInstance("John", 30);
            // 获取字段名称
            String fieldName = "name";
            // 获取字段对象
            Field field = clazz.getDeclaredField(fieldName);
            // 设置字段可访问(如果字段是私有的,需要设置为可访问才能获取和修改值)
            field.setAccessible(true);
            // 设置字段值
            field.set(obj, "Alice");
            // 获取修改后的字段值
            Object fieldValue = field.get(obj);
            System.out.println("Modified field value: " + fieldValue);
        } catch (ClassNotFoundException | NoSuchMethodException | NoSuchFieldException |
                 IllegalAccessException | InstantiationException | InvocationTargetException e) {
            e.printStackTrace();
        }
    }
}
class Person {
    private String name;
    private int age;
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

53.如何决定使用 HashMap 还是 TreeMap?

  1. TreeMap<K,V>的Key值是要求实现java.lang.Comparable,所以迭代的时候TreeMap默认是按照Key值升序排序的,TreeMap的实现是基于红黑树结构。适用于按自然顺序或自定义顺序遍历键(key)

  2. HashMap<K,V>的Key值实现散列hashCode(),分布是散列的、均匀的,不支持排序;数据结构主要是数组,链表或红黑树。适用于在Map中插入、删除和定位元素

  3. 结论:如果你需要得到一个有序的结果时就应该使用TreeMap(因为HashMap中元素的排列顺序是不固定的)。除此之外,由于HashMap有更好的性能,所以大多不需要排序的时候我们会使用HashMap

54.如何实现数组和 List 之间的转换?

  1. List转换成为数组:调用ArrayList的toArray方法
package com.example.demo.myTest;
import java.util.ArrayList;
import java.util.List;
public class Test01 {
    public static void main(String[] args) {
        List<String> strings = new ArrayList<>();
        strings.add("12");
        strings.add("13");
        strings.add("14");
        strings.add("1");
        strings.add("62");
        strings.add("52");
        String[] strings1 = strings.toArray(new String[strings.size()]);
        for (int i = 0; i < strings1.length; i++) {
            System.out.println(strings1[i]);
        }
        System.out.println("****************************");
        for (String s : strings1) {
            System.out.println(s);
        }
    }
}
  1. 数组转换成为List:调用Arrays的asList方法
package com.example.demo.myTest;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class Test01 {
    public static void main(String[] args) {
        String[] strings = new String[5];
        for (int i = 0; i < 5; i++) {
            strings[i] = i + "";
        }
        List<String> stringList = Arrays.asList(strings);
        for (int i = 0; i < stringList.size(); i++) {
            System.out.println(stringList.get(i));
        }
    }
}

55.Array 和 ArrayList 有何区别?

  1. Array 数组可以包含基本类型和对象类型,ArrayList 却只能包含对象类型。Array 数组在存放的时候一定是同种类型的元素。ArrayList 就不一定了
  2. Array 数组的空间大小是固定的,所以需要使用前确定合适的空间大小。ArrayList 的空间是动态增长的,而且,每次添加新的元素的时候都会检查内部数组的空间是否足够
  3. ArrayList 方法上比 Array 更多样化,比如添加全部 addAll()、删除全部 removeAll()、返回迭代器 iterator() 等

56.集合和数组的区别?

  1. 数组是固定长度的;集合可变长度的
  2. 数组可以存储基本数据类型,也可以存储引用数据类型;集合只能存储引用数据类型
  3. 数组存储的元素必须是同一个数据类型;集合存储的对象可以是不同数据类型

57.怎么确保一个集合不能被修改?

可以使用 Collections. unmodifiableCollection(Collection c) 方法来创建一个只读集合,这样改变集合的任何操作都会抛出 Java. lang. UnsupportedOperationException 异常

58.如何边遍历边移除 Collection 中的元素?

边遍历边移除 Collection 的唯一正确方式是使用 Iterator.remove() 方法

59.遍历一个 List 有哪些不同的方式?每种方法的实现原理是什么?Java 中 List 遍历的最佳实践是什么?

  1. for 循环遍历,基于计数器。在集合外部维护一个计数器,然后依次读取每一个位置的元素,当读取到最后一个元素后停止

  2. 迭代器遍历,Iterator。Iterator 是面向对象的一个设计模式,目的是屏蔽不同数据集合的特点,统一遍历集合的接口。Java 在 Collections 中支持了 Iterator 模式

  3. foreach 循环遍历。foreach 内部也是采用了 Iterator 的方式实现,使用时不需要显式声明 Iterator 或计数器。优点是代码简洁,不易出错;缺点是只能做简单的遍历,不能在遍历过程中操作数据集合,例如删除、替换

  4. 最佳实践:Java Collections 框架中提供了一个 RandomAccess 接口,用来标记 List 实现是否支持 Random Access。如果一个数据集合实现了该接口,就意味着它支持 Random Access,按位置读取元素的平均时间复杂度为 O(1),如ArrayList。如果没有实现该接口,表示不支持 Random Access,如LinkedList

  5. 推荐的做法就是:支持 Random Access 的列表可用 for 循环遍历,否则建议用 Iterator 或 foreach 遍历

60.迭代器 Iterator 是什么?

迭代器是一种设计模式,它是一个对象,它可以遍历并选择序列中的对象,但是该对象比较特殊,不能直接创建对象,该对象是以内部类的形式存在于每个集合类的内部。迭代器通常被称为“轻量级”对象,因为创建它的代价小

61.Iterator 怎么使用?有什么特点?

  1. iterator只能单向移动。使用方法iterator()要求容器返回一个Iterator。第一次调用Iterator的next()方法时,它返回序列的第一个元素。注意:iterator()方法是java.lang.Iterable接口,被Collection继承。使用next()获得序列中的下一个元素。

  2. terator在遍历元素过程中,有线程修改集合元素会有ConcurrentModificationEception异常

62.Iterator 和 ListIterator 的区别?

  1. Iterator可用来遍历Set和List集合,但是ListIterator只能用来遍历List,而Set并不能使用ListIterator

  2. ListIterator有add()方法,可以向List中添加对象,而Iterator不能

  3. ListIterator和Iterator都有hasNext()和next()方法,可以实现顺序向后遍历。但是ListIterator有hasPrevious()和previous()方法,可以实现逆向(顺序向前)遍历。Iterator就不可以

  4. ListIterator可以定位当前的索引位置,nextIndex()和previousIndex()可以实现。Iterator 没有此功能

  5. 都可实现删除对象,但是ListIterator可以实现对象的修改,set()方法可以实现。Iterator仅能遍历,不能修改。因为ListIterator的这些功能,可以实现对LinkedList等List数据结构的操作

62.说一下 ArrayList 的优缺点?

  • 优点:(1)ArrayList 底层以数组实现,是一种随机访问模式。ArrayList 实现了 RandomAccess 接口,因此查找的时候非常快.(2)ArrayList在顺序添加一个元素的时候非常方便

  • 缺点:(1)删除元素的时候,需要做一次元素复制操作。如果要复制的元素很多,那么就会比较耗费性能.(2)插入元素的时候,也需要做一次元素复制操作,缺点同上

  • 结论:ArrayList 比较适合顺序添加、随机访问的场景

63.多线程场景下如何使用 ArrayList?

ArrayList 不是线程安全的,如果遇到多线程场景,可以通过 Collections 的 synchronizedList 方法将其转换成线程安全的容器后再使用

package com.company.test01;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class Test05 {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        // 主要是这个方法
        List<String> list1 = Collections.synchronizedList(list);
        list1.add("12");
        list1.add("13");
        list1.add("14");
        list1.add("15");
        list1.add("16");
        list1.add("17");
        for (int i = 0; i < list1.size(); i++) {
            System.out.println(list1.get(i));
        }
    }
}

64.为什么 ArrayList 的 elementData 加上 transient 修饰?

ArrayList 实现了 Serializable 接口,这意味着 ArrayList 支持序列化。transient 的作用是说不希望 elementData 数组被序列化.重写了 writeObject 实现:每次序列化时,先调用 defaultWriteObject() 方法序列化 ArrayList 中的非 transient 元素,然后遍历 elementData,只序列化已存入的元素,这样既加快了序列化的速度,又减小了序列化之后的文件大小

65.HashSet如何检查重复?HashSet是如何保证数据不可重复的?======+1

  1. 检查重复:向HashSet 中add ()元素时,判断元素是否存在的依据,不仅要比较hash值,同时还要结合equles 方法比较。HashSet 中的add ()方法会使用HashMap 的put()方法
  2. 保证数据不重复:HashMap 的 key 是唯一的,由源码可以看出 HashSet 添加进去的值就是作为HashMap 的key,并且在HashMap中如果K/V相同时,会用新的V覆盖掉旧的V,然后返回新的V。所以不会重复( HashMap 比较key是否相等是先比较hashcode 再比较equals )

66.为什么HashMap中String、Integer这样的包装类适合作为K?

  1. String、Integer等包装类的特性能够保证Hash值的不可更改性和计算准确性,能够有效的减少Hash碰撞的几率
  2. 都是final类型,即不可变性,保证key的不可更改性,不会存在获取hash值不同的情况
  3. 内部已重写了equals()、hashCode()等方法,遵守了HashMap内部的规范,不容易出现Hash值计算错误的情况

67.如果使用Object作为HashMap的Key,应该怎么办呢?

重写hashCode和equals方法

68.HashSet和TreeSet分别如何实现去重的?

  1. HashSet:添加元素时,先得到hash值,会转成索引值,找到存储数据表table,看这个索引位置是否有元素,如果没有,则直接加入。如果有元素,即调用equals比较(记得要重写equals),如果相同,就放弃添加,如果不相同,就将其放到最后。如果添加时发现容量不够,开始扩容。
  2. TreeSet去重机制:如果你传入的一个Comparator匿名对象,就使用实现的comparator去重,如果方法返回0,就认为是相同的元素,就不添加,如果你没有传入一个Comparator匿名对象,则以你添加的对象实现的Comparable接口里的compareTo方法去重

69.创建一个User类,TreeSet treeSet = new TreeSet();treeSet.add(new User);会不会报错?为什么?

首先会不会报错取决于你的这个User类有没有实现Comparable接口,如果没有去实现这个接口,那么在TreeSet使用add方法的时候,它的底层会去将User类转成Comparable类型。这时候,你没实现这个接口,那么会报出一个类型异常的错误

70.java创建对象有几种方式?========+1

  1. 通过new关键字创建对象
  2. 通过反射创建对象
  3. 通过clone创建对象
  4. 通过序列化创建对象

71.java的克隆实现方式以及理解?克隆针对的是类还是对象?======+1

  1. 克隆的对象必须实现Cloneable这个接口,而且需要重写clone方法,重写时将该方法由受保护变为公开。 一个实现了Cloneable类意味着可以通过java.lang.Object的clone()合法的对该类的实例的属性逐一复制。没有实现Cloneable接口的类的实例调用clone()会抛出CloneNotSupportedException。克隆针对的是对象!
  2. 基本数据类型克隆时只是传值,不存在传引用。没有实现Cloneable接口的类的实例克隆是通过传引用实现的,String这个情况特殊,理解String不可变原理即可。可以看看这篇文章
  3. 浅克隆:
package com.company.test01;
public class TestClone {
    public static void main(String[] args) throws CloneNotSupportedException {
        TestA testA = new TestA(1, "1437");
        Test test = new Test();
        test.setName("13");
        test.setAge(19);
        test.setTestA(testA);
        // 克隆test
        Test test1 = test.clone();
        System.out.println(test1);
        System.out.println(test);
        // 修改test1的值观看变化
        test1.setAge(21);
        test1.setName("14");
        test1.getTestA().setId(2);
        test1.getTestA().setName("201437");
        System.out.println(test1);
        System.out.println(test);
    }
}
class Test implements Cloneable{
    private String name;
    private int age;
    private TestA testA;
    public Test() {
    }
    public Test(String name, int age, TestA testA) {
        this.name = name;
        this.age = age;
        this.testA = testA;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
    public TestA getTestA() {
        return testA;
    }
    public void setTestA(TestA testA) {
        this.testA = testA;
    }
    @Override
    public String toString() {
        return "Test{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", testA=" + testA +
                '}';
    }
    @Override
    protected Test clone() throws CloneNotSupportedException {
        Test test = null;
        test = (Test)super.clone();
        return test;
    }
}
class TestA{
    private int id;
    private String name;
    public TestA() {
    }
    public TestA(int id, String name) {
        this.id = id;
        this.name = name;
    }
    public int getId() {
        return id;
    }
    public void setId(int id) {
        this.id = id;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    @Override
    public String toString() {
        return "TestA{" +
                "id=" + id +
                ", name='" + name + '\'' +
                '}';
    }
}

// 打印结果--可以看到,Test对象的name属性并没有变化,但是age属性和testA对象的值是跟着一起变化了
// 这里打印的是没有修改任何值的时候
Test{name='13', age=19, testA=TestA{id=1, name='1437'} }
Test{name='13', age=19, testA=TestA{id=1, name='1437'} }
// 这里打印的是test1修改值之后
Test{name='14', age=21, testA=TestA{id=2, name='201437'} }
Test{name='13', age=19, testA=TestA{id=2, name='201437'} }
  1. 深克隆:
package com.company.test01;
public class TestCloneDeep {
    public static void main(String[] args) throws CloneNotSupportedException {
        TestDeepB testDeepB = new TestDeepB(21, "581437");
        TestDeepA testDeepA = new TestDeepA();
        testDeepA.setAge(18);
        testDeepA.setName("1437");
        testDeepA.setTestDeepB(testDeepB);
        // 进行克隆
        TestDeepA testDeepA1 = (TestDeepA) testDeepA.clone();
        System.out.println(testDeepA);
        System.out.println(testDeepA1);
        // 修改testDeepA1的值
        testDeepA1.setAge(50);
        testDeepA1.setName("201437");
        testDeepA1.getTestDeepB().setAge(51);
        testDeepA1.getTestDeepB().setName("58143700");
        // 修改完后打印
        System.out.println(testDeepA);
        System.out.println(testDeepA1);
    }
}
class TestDeepA implements Cloneable {
    private int age;
    private String name;
    private TestDeepB testDeepB;
    public TestDeepA() {
    }
    public TestDeepA(int age, String name, TestDeepB testDeepB) {
        this.age = age;
        this.name = name;
        this.testDeepB = testDeepB;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public TestDeepB getTestDeepB() {
        return testDeepB;
    }
    public void setTestDeepB(TestDeepB testDeepB) {
        this.testDeepB = testDeepB;
    }
    @Override
    public String toString() {
        return "TestDeepA{" +
                "age=" + age +
                ", name='" + name + '\'' +
                ", testDeepB=" + testDeepB +
                '}';
    }
    @Override
    protected Object clone() throws CloneNotSupportedException {
        // 这里实现第一步,只是一个浅克隆
        TestDeepA testDeepA = null;
        testDeepA = (TestDeepA)super.clone();
        // 这里使用双层克隆,让TestDeepB也实现克隆
        testDeepA.setTestDeepB((TestDeepB)testDeepA.getTestDeepB().clone());
        return testDeepA;
    }
}
class TestDeepB implements Cloneable {
    private int age;
    private String name;
    public TestDeepB() {
    }
    public TestDeepB(int age, String name) {
        this.age = age;
        this.name = name;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    @Override
    public String toString() {
        return "TestDeepB{" +
                "age=" + age +
                ", name='" + name + '\'' +
                '}';
    }
    @Override
    protected Object clone() throws CloneNotSupportedException {
        TestDeepB testDeepB = null;
        testDeepB = (TestDeepB)super.clone();
        return testDeepB;
    }
}
// 打印结果--可以看到,只是克隆的那个值发生了变化
TestDeepA{age=18, name='1437', testDeepB=TestDeepB{age=21, name='581437'} }
TestDeepA{age=18, name='1437', testDeepB=TestDeepB{age=21, name='581437'} }
TestDeepA{age=18, name='1437', testDeepB=TestDeepB{age=21, name='581437'} }
TestDeepA{age=50, name='201437', testDeepB=TestDeepB{age=51, name='58143700'} }
  1. 利用serializable实现深复制:
package com.company.test01;
import java.io.*;
public class TestCloneDeep01 {
    public static void main(String[] args) throws Exception {
        TestDeep01B testDeep01B = new TestDeep01B("1437", 18);
        TestDeep01A testDeep01A = new TestDeep01A("201437", 28, testDeep01B);
        // 克隆
        TestDeep01A testDeep01A1 = CloneUtils.clone(testDeep01A);
        // 打印
        System.out.println(testDeep01A);
        System.out.println(testDeep01A1);
        // 修改testDeep01A1数据
        testDeep01A1.setName("581437");
        testDeep01A1.setAge(58);
        testDeep01A1.getTestDeep01B().setName("13");
        testDeep01A1.getTestDeep01B().setAge(60);
        // 修改完打印
        System.out.println(testDeep01A);
        System.out.println(testDeep01A1);
    }
}
class TestDeep01A implements Serializable {
    private static final long UID = -3936148364278781437L;
    private String name;
    private int age;
    private TestDeep01B testDeep01B;
    public TestDeep01A() {
    }
    public TestDeep01A(String name, int age, TestDeep01B testDeep01B) {
        this.name = name;
        this.age = age;
        this.testDeep01B = testDeep01B;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
    public TestDeep01B getTestDeep01B() {
        return testDeep01B;
    }
    public void setTestDeep01B(TestDeep01B testDeep01B) {
        this.testDeep01B = testDeep01B;
    }
    @Override
    public String toString() {
        return "TestDeep01A{" +
                "name='" + name + '\'' +
                ", age='" + age + '\'' +
                ", testDeep01B=" + testDeep01B +
                '}';
    }
}
class TestDeep01B implements Serializable {
    private static final long UID = -4482468804013491437L;
    private String name;
    private int age;
    public TestDeep01B() {
    }
    public TestDeep01B(String name, int age) {
        this.name = name;
        this.age = age;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
    @Override
    public String toString() {
        return "TestDeep01B{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}
// 工具类
class CloneUtils {
    public static <T extends Serializable> T clone(T o) throws Exception {
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
        objectOutputStream.writeObject(o);
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
        ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
        return (T)objectInputStream.readObject();
    }
}
// 打印结果
TestDeep01A{name='201437', age=28, testDeep01B=TestDeep01B{name='1437', age=18} }
TestDeep01A{name='201437', age=28, testDeep01B=TestDeep01B{name='1437', age=18} }
TestDeep01A{name='201437', age=28, testDeep01B=TestDeep01B{name='1437', age=18} }
TestDeep01A{name='581437', age=58, testDeep01B=TestDeep01B{name='13', age=60} }

72.java的参数传递方式是什么?=======+2

  1. java使用的是值传递。
  2. java中只有值传递,基本类型传递的是值的副本,引用类型传递的是引用的副本。可以看看这个文章

73.synchronized 的作用? ======+1

在 Java 中,synchronized 关键字是用来控制线程同步的,就是在多线程的环境下,控制 synchronized 代码段不被多个线程同时执行;synchronized 可以修饰类、方法、变量。

74.synchronized 和 Lock 有什么区别?======+1

  1. 首先synchronized是Java内置关键字,在JVM层面,Lock是个Java类
  2. synchronized 可以给类、方法、代码块加锁;而 lock 只能给代码块加锁
  3. synchronized 不需要手动获取锁和释放锁,使用简单,发生异常会自动释放锁,不会造成死锁;而 lock 需要自己加锁和释放锁,如果使用不当没有 unLock()去释放锁就会造成死锁
  4. 通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到

75.jdk1.8对hashMap做了哪些优化?======+1

  1. 最重要的一点是底层结构不一样,1.7是数组+链表,1.8则是数组+链表+红黑树结构
  2. 插入键值对的put方法的区别,1.8中会将节点插入到链表尾部,而1.7中是采用头插;
  3. 扩容机制:1.7中是只要不小于阈值就直接扩容2倍;在jdk8版本的时候haspMap在第一次添加数据时,默认构造函数构建的初始容量是16,当达到它的临界值(0.75)的时候,数组就会扩容,如果有一条链表的元素个数到达8,且数组的大小到达64时。就会进化成红黑树,当数组的长度重新低于6的时候,又会将红黑树重新转换为链表

76.java多线程的实现方式?======+2

  1. 继承Thread并重写run方法,调用start方法
  • 继承Thread类是最常见的实现方式之一。可以通过创建一个继承自Thread类的子类,并重写run()方法来实现多线程的逻辑。重写run()方法后,需要创建该子类的实例,并通过调用start()方法来启动线程。此时,该实例的run()方法将在新线程中执行。例如:

  • PS: 需要注意的是,不能直接调用子类的run()方法来启动线程,因为这样会在当前线程中执行run()方法,而不是在新线程中执行

package multithreading;
public class TestThread {
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            Thread thread = new Test01();
            thread.start();
        }
    }
}
class Test01 extends Thread {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }
}
  1. 实现Runnable接口,并用其初始化Thread,然后创建Thread实例,调用start方法
  • 除了继承Thread类,还可以实现Runnable接口来实现多线程。可以创建一个实现了Runnable接口的类,并在该类中实现run()方法。然后,在创建Thread对象时,将该类的实例作为参数传入即可,然后调用start方法。

  • PS:需要注意的是,实现Runnable接口只是定义了一个任务,而不是线程。在创建Thread对象时,需要将该任务传递给Thread构造函数。

  • 和继承Thread类相比,实现Runnable接口有以下优点:

 可以避免Java单继承的限制。
  可以将任务从线程控制分离出来。
package multithreading;
public class TestRunnable {
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            Thread thread = new Thread(new Test02());
            thread.start();
        }
    }
}
class Test02 implements Runnable {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }
}
  1. 实现Callable接口,并用其初始化Thread,然后创建Thread实例,调用start方法
  • 实现Callable接口时,需要重写call()方法,并在里面编写线程的业务逻辑

  • 创建一个ExecutorService类型的线程池,使用newFixedThreadPool()方法创建一个指定大小的线程池。例如代码:

  • 将Callable对象提交到线程池中执行,并获取Future对象。例如代码

  • 在需要时通过Future对象获取线程执行结果。例如:

  • 最后切记要关闭线程池

package multithreading;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class TestCallable {
    public static void main(String[] args) throws Exception {
        for (int i = 0; i < 10; i++) {
            //创建Test03实例
            Callable<Boolean> callable1 = new Test03();
            Callable<Boolean> callable2 = new Test03();
            Callable<Boolean> callable3 = new Test03();
            // 创建执行服务
            ExecutorService executorService = Executors.newFixedThreadPool(3);
            // 提交执行
            Future<Boolean> submit1 = executorService.submit(callable1);
            Future<Boolean> submit2 = executorService.submit(callable2);
            Future<Boolean> submit3 = executorService.submit(callable3);
            // 获取结果
            Boolean aBoolean1 = submit1.get();
            Boolean aBoolean2 = submit2.get();
            Boolean aBoolean3 = submit3.get();
            System.out.println(aBoolean1);
            System.out.println(aBoolean2);
            System.out.println(aBoolean3);
            // 关闭服务
            executorService.shutdownNow();
        }
    }
}
class Test03 implements Callable<Boolean> {
    @Override
    public Boolean call() throws Exception {
        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + "c:" + i);
        }
        return true;
    }
}
  1. 使用线程池创建
  • Java提供了Executors类来创建线程池,常用的有newFixedThreadPool()、newCachedThreadPool()和newSingleThreadExecutor()等方法。例如,创建一个固定大小的线程池:具体如下代码

  • 创建Runnable或Callable任务需要执行的任务可以是实现了Runnable接口或Callable接口的类。例如:

  • 提交任务到线程池,使用submit()方法将任务提交到线程池中。例如

  • 关闭线程池

  • PS:需要注意的是,在使用带有返回值的线程时,还可以通过isDone()方法来判断目标线程是否已经执行完毕

import java.util.concurrent.*;
public class TestThreadPool {
    public static void main(String[] args) throws Exception {
        // 创建线程池
        ExecutorService executorService = Executors.newFixedThreadPool(3);
        // 提交任务到线程池
        Runnable myRunnable = new MyRunnable();
        Future<?> future1 = executorService.submit(myRunnable);
        Callable<String> myCallable = new MyCallable();
        Future<String> future2 = executorService.submit(myCallable);
        // 获取任务执行结果
        Object result1 = future1.get();
        String result2 = future2.get();
        // 关闭线程池
        executorService.shutdown();
    }
}
class MyRunnable implements Runnable {
    @Override
    public void run() {
        // 线程的业务逻辑代码
    }
}
class MyCallable implements Callable<String> {
    @Override
    public String call() throws Exception {
        // 线程的业务逻辑代码
        return "线程执行完毕";
    }
}

77.while循环和for循环的区别?======+1

78.break,continue,return,有什么区别?=======+2

PS:这两个问题很奇怪,这么简单的问题为什么会被问两次

79.悲观锁和乐观锁的区别?

  1. 悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其他线程阻塞,用完后在把资源转让给其他线程)
  2. 总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现
  3. 乐观锁常见的两种实现方式:
  • 版本号机制:
    一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值会+1.当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值为当前数据库的version值相等时才更新,否则重试更新操作,直到更新成功
  • 使用时间戳:跟版本号一样的逻辑

PS:还有一种CAS方法解决

80.java里重写equals时为什么也要重写hashcode?

  1. 在 Java 中这两个方法用于在处理哈希表(例如 HashMap、HashSet 等)时确定对象的相等性。简单来说就是为了确保它们保持一致。

  2. 简单说一下哈希表:在哈希表中,对象的哈希码被用作索引来查找对象的存储位置。当你调用contains()、get()、remove() 等方法时,哈希表会首先使用对象的哈希码来确定对象所在的桶(bucket),然后再使用 equals() 方法来比较对象是否相等。

  3. 如果你只重写了 equals() 方法而没有重写 hashCode() 方法,可能会导致以下问题:

  • 不一致的行为:根据 equals() 方法比较为相等的两个对象,它们的哈希码可能不同,这可能导致它们被放置在哈希表中的不同位置。这将使得在使用哈希表时无法正确地定位对象。
  • 违反哈希表的合同:如果你不重写 hashCode() 方法,它将继承自 Object 类的默认实现,该实现基于对象的存储地址生成哈希码,与 equals() 方法的逻辑无关。这将违反合同,可能导致在使用哈希表时出现意想不到的行为。

PS: 为了确保对象在哈希表中的正确存储和检索,你需要根据对象的相等性逻辑来生成相应的哈希码。通常,你应该在重写 equals()方法的同时,根据对象的状态生成哈希码,最好是使用对象的属性值计算哈希码。这样可以确保具有相等状态的对象具有相等的哈希码,进而使得哈希表能够正确地存储和检索对象。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

你不懂、、、

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值