Java 中的基本数据类型有哪些?

Java 中的数据类型可以分为两大类:基本数据类型和引用数据类型。

基本数据类型是 Java 中的原始数据类型,而引用数据类型则是对象引用。

基本数据类型(Primitive Data Types)

  • 整型:byte(8位有符号整数)、short(16位有符号整数)、int(32位有符号整数)、long(64位有符号整数,后缀 L 或 l)。
  • 浮点型:float(32位浮点数,后缀 F 或 f)、double(64位浮点数,后缀 D 或 d)。
  • 字符型:char(16位 Unicode 字符)。
  • 布尔型:boolean(只有两个可能的值:true 或 false)。

引用数据类型(Reference Data Types)

  • 类(Class):用户自定义的类或 Java 内置的类。
  • 接口(Interface):定义了类必须实现的方法的契约。
  • 数组(Array):一种容器对象,可以包含固定数量的单一类型值。
  • 枚举(Enumeration):用于表示一组预定义的常量,使代码更加简洁、可读。
  • 注解(Annotation):修饰方法或者类或者属性。

Java 中的参数传递是按值还是按引用?

Java 只有按值传递,不论是基本类型还是引用类型。

JVM 内存有划分为栈和堆,局部变量和方法参数是在上分配的 ,引用类型占 4 个字节,基本类型看具体类型,例如 long 和 double 占 8 个字节。

而对象所占的空间是在中开辟的,引用类型变量存储的是对象在堆中的地址,并以此来访问对象,所以传递的时候可以理解为把变量存储的地址给传递过去,因此引用类型也是值传递。

Java 中的深拷贝和浅拷贝有什么区别?

深拷贝:完全拷贝一个对象,包括基本类型和引用类型,堆内的引用对象也会复制一份。

浅拷贝:仅拷贝基本类型和引用,堆内的引用对象和被拷贝的对象共享。

所以假如拷贝的对象成员间有一个 list,深拷贝之后堆内有 2 个 list,之间不会影响,而浅拷贝的话堆内还是只有一个 list。

比如现在有个 teacher 对象,然后成员里面有一个 student 列表。

image-20210303201307397.png

因此深拷贝是安全的,浅拷贝的话如果有引用对象则原先和拷贝对象修改引用对象的值会相互影响。

Java 的类加载过程是怎样的?

类加载顾名思义就是把类加载到 JVM 中,而输入一段二进制流到内存,之后经过一番解析、处理转化成可用的 class 类,这就是类加载要做的事情。

二进制流可以来源于 class 文件,或者通过字节码工具生成的字节码或者来自于网络都行,只要符合格式的二进制流,JVM 来者不拒。

类加载流程分为加载、连接、初始化三个阶段,连接还能拆分为:验证、准备、解析三个阶段。

所以总的来看可以分为 5 个阶段:

  • 加载:将二进制流搞到内存中来,生成一个 Class 类。
  • 验证:主要是验证加载进来的二进制流是否符合一定格式,是否规范,是否符合当前 JVM 版本等等之类的验证。
  • 准备:为静态变量(类变量)赋初始值,也即为它们在方法区划分内存空间。这里注意是静态变量,并且是初始值,比如 int 的初始值是 0。
  • 解析:将常量池的符号引用转化成直接引用。符号引用可以理解为只是个替代的标签,比如你此时要做一个计划,暂时还没有人选,你设定了个 A 去做这个事。然后等计划真的要落地的时候肯定要找到确定的人选,到时候就是小明去做一件事。解析就是把 A(符号引用) 替换成小明(直接引用)。符号引用就是一个字面量,没有什么实质性的意义,只是一个代表。直接引用指的是一个真实引用,在内存中可以通过这个引用查找到目标。
  • 初始化:这时候就执行一些静态代码块,为静态变量赋值,这里的赋值才是代码里面的赋值,准备阶段只是设置初始值占个坑。

什么是 Java 中的不可变类?如何实现一个不可变类?

不可变类指的是无法修改对象的值,比如 String 就是典型的不可变类,当你创建一个 String 对象之后,这个对象就无法被修改。所以不要在字符串拼接频繁的场景使用 + 来拼接,因为这样会频繁的创建对象。

至于如何实现,就参考 String 的设计就行。

String 类用 final 修饰,表示无法被继承。

String 本质是一个 char 数组,然后用 final 修饰,不过 final 限制不了数组内部的数据,所以这还不够。

所以 value 是用 private 修饰的,并且没有暴露出 set 方法,这样外部其实就接触不到 value 所以无法修改。

当然还是有修改的需求,比如 replace 方法,所以这时候就需要返回一个新对象来作为结果。

总结一下就是私有化变量,然后不要暴露 set 方法,即使有修改的需求也是返回一个新对象。

Java 中 String、StringBuffer 和 StringBuilder 的区别是什么?

String 是 Java 中基础且重要的类,是可不变的。

StringBuffer 就是为了解决大量拼接字符串时产生很多中间对象问题而提供的一个类,它的本质是一个线程安全的可修改的字符序列,把所有修改数据的方法都加上了 synchronized。但是保证了线程安全是需要性能的代价的。

在很多情况下我们的字符串拼接操作不需要线程安全,这时候 StringBuilder 登场了,StringBuilderJDK1.5发布的,它和 StringBuffer 本质上没什么区别,就是去掉了保证线程安全的那部分,减少了开销

选择建议

  • String:适用于少量字符串操作或需要字符串常量池优化的场景。
  • StringBuffer:适用于多线程环境下频繁的字符串操作。
  • StringBuilder:适用于单线程环境下频繁的字符串操作。

使用 new String() 语句在 Java 中会创建多少个对象?

会创建 1 或 2 个字符串对象。

主要有两种情况:

1、如果字符串常量池中不存在字符串对象的引用,那么它会在堆上创建两个字符串对象,其中一个字符串对象的引用会被保存在字符串常量池中。

2、如果字符串常量池中已存在字符串对象“yupi”的引用,则只会在堆中创建 1 个字符串对象“yupi”。

// 字符串常量池中已存在字符串对象的引用
String s1 = "test";
// 下面这段代码只会在堆中创建 1 个字符串对象
String s2 = new String("test");

为什么 JDK 9 中将 String 的 char 数组改为 byte 数组?

这个操作主要是为了节省内存空间,提高内存利用率

在 JDK 9 之前,String 类是基于 char[] 实现的,内部采用 UTF-16 编码,每个字符占用两个字节

但是,如果当前的字符仅需一个字节的空间,这就造成了浪费。例如一些 Latin-1 字符用一个字节即可表示。

因此 JDK 9 做了优化采用 byte 数组来实现。

并引入了 coder 变量来标识编码方式(Latin-1 或 UTF-16)。对于大多数只包含 Latin-1 字符(即每个字符可以用一个字节表示)的字符串,内存使用量减半。

Latin1 是什么?

Latin1 是国际标准编码 ISO-8859-1 的别名。Latin1 也是单字节编码,在 ASCII 编码的基础上,利用了 ASCII 未利用的最高位,扩充了 128 个字符,因此 Latin1 可以表示 256 个字符,并向下兼容 ASCII。

Latin1收录的字符除 ASCII 收录的字符外,还包括西欧语言、希腊语、泰语、阿拉伯语、希伯来语对应的文字符号。欧元符号出现的比较晚,没有被收录在 ISO-8859-1 当中,在后来的修订版 ISO-8859-15 加入了欧元符号。

Latin1的编码范围是 0x00-0xFF,ASCII的编码范围是 0x00-0x7F。

Latin1 相对 ASCII 而言,较少被提及,其实 Latin1 的使用还是比较广泛的,比如 MySQL(8.0之前)的数据表存储默认编码就是 Latin1。

Java 的 BigDecimal 是什么?

BigDecimal 是 Java 中提供的一个用于高精度计算的类,属于 java.math 包。它提供对浮点数和定点数的精确控制,特别适用于金融和科学计算等需要高精度的领域。

主要特点:

  • 高精度:BigDecimal 可以处理任意精度的数值,而不像 float 和 double 存在精度限制。
  • 不可变性:BigDecimal 是不可变类,所有的算术运算都会返回新的 BigDecimal 对象,而不会修改原有对象(所以要注意性能问题)。
  • 丰富的功能:提供了加、减、乘、除、取余、舍入、比较等多种方法,并支持各种舍入模式。

通常情况下,大部分需要浮点数精确运算结果的业务场景(比如涉及到钱的场景)都是通过 BigDecimal 来做的。

Java 中包装类型和基本类型的区别是什么?

Java 中有 8 种基本数据类型,这些基本类型又有对应的包装类。

基本数据类型(默认值) 包装类(默认值) 长度 取值范围
byte (0) Byte (null) 1字节 -128~127
short (0) Short (null) 2字节 -32768~32767
int (0) Integer (null) 4字节 -231 ~ 2(31-1)
long (0) Long (null) 8字节 -263 ~ 2(63-1)
float (0.0) Float (null) 4字节 1.4E-45~3.4028235E38
double (0.0) Double (null) 8字节 4.9E-324~1.7976931348623157E308
boolean (false) Boolean (null) / true | false
char (为空) Character (null) 2字节 0~65535

因为 Java 是一种面向对象语言,很多地方都需要使用对象而不是基本数据类型。比如,在集合类中,我们是无法将int 、double等类型放进去的。因为集合的容器要求元素是 Object类型。

为了让基本类型也具有对象的特征,就出现了包装类型,它相当于将基本类型“包装起来”,使得它具有了对象的性质,并且为其添加了属性和方法,丰富了基本类型的操作。

基本类型与包装类型的区别

  1. 默认值不同:基本类型的默认值是 0,false 等,包装类默认为 null(包装类型可以为null,而基本类型不可以)
  2. 初始化的方式不同:一个需要采用 new 的方式创建,一个则不需要
  3. 存储方式有所差异:基本类型主要保存在上面,包装类对象保存在上(成员变量的话,在不考虑JIT优化的栈上分配时,都是随着对象一起保存在堆上的)
  4. 包装类型可用于泛型,而基本类型不可以
  5. 两个包装类型的值可以相同,但却不相等(==)

什么是 Java 中的自动装箱和拆箱?

自动装箱(Autoboxing)和拆箱(Unboxing)是 Java 语言中的一种特性,它们允许自动地在基本数据类型和相应的包装类之间进行转换。极大地简化了代码,使得基本类型和包装类之间的转换更加透明和自然。

需要注意的事项:

  1. 性能影响:虽然自动装箱和拆箱提供了方便,但它们会产生额外的对象创建和拆箱操作,可能会影响性能,尤其是在循环或频繁使用的场景中
  2. NullPointerException:在进行拆箱操作时,如果包装类对象为 null,会抛出 NullPointerException。

什么是 Java 的 Integer 缓存池?

因为根据实践发现大部分的数据操作都集中在值比较小的范围,因此 Integer 搞了个缓存池,默认范围是 -128 到 127,可以根据通过设置 JVM-XX:AutoBoxCacheMax=<size> 来修改缓存的最大值,最小值改不了。

实现的原理是 int 在自动装箱的时候会调用Integer.valueOf,进而用到了 IntegerCache。

image-20210228112742081.png

IntegerCache 在静态块中会初始化好缓存值。

image-20210228112757226.png

所以这里还有个面试题,就是啥 Integer 127 之内的相等,而超过 127 的就不等了,因为 127 之内的就是同一个对象,所以当然相等。

不仅 Integer 有,Long 也是有的,不过范围是写死的 -128 到 127。

image-20210228112817173.png

对了 Float 和 Double 是没有滴,毕竟是小数,能存的数太多了。

Java 中 hashCode 和 equals 方法是什么?它们与 == 操作符有什么区别?

这些都是 Java 中用于比较对象的三种方式。

hashCode

hashCode返回对象的哈希码(整数),主要用于支持基于哈希表的集合,用来确定对象的存储位置,如 HashMap、HashSet 等。

Object 类中的默认实现会根据对象的内存地址生成哈希码(native 方法)。

Java 中,hashCode 方法和 equals 方法之间有一个 “合约”

  • 如果两个对象根据 equals 方法被认为是相等的,那么它们必须具有相同的哈希码
  • 如果两个对象具有相同的哈希码,它们并不一定相等,但会被放在同一个哈希桶中。

equals

用于比较两个对象的内容是否相等Object 类中的默认实现会使用 == 操作符来比较对象的内存地址。

通常我们需要在自定义类中重写 equals 方法,以基于对象的属性进行内容比较。比如你可以自定义两个对象的名字一样就是相等的、年龄一样就是相等,可以灵活按照需求定制。

对于 equals 定义的比较,实际上还有以下五个要求:

  • 自反性:对于任何非空引用值 xx.equals(x) 必须返回 true
  • 对称性:对于任何非空引用值 xy,如果 x.equals(y) 返回 true,则 y.equals(x) 也必须返回 true
  • 传递性:对于任何非空引用值 xyz,如果 x.equals(y) 返回 truey.equals(z) 返回 true,则 x.equals(z) 也必须返回 true
  • 一致性:对于任何非空引用值 xy,只要对象在比较中没有被修改,多次调用 x.equals(y) 应返回相同的结果。
  • 对于任何非空引用值 xx.equals(null) 必须返回 false

==

== 操作符用于比较两个引用是否指向同一个对象(即比较内存地址),如果是基本数据类型,== 直接比较它们的值。

区别总结

hashCode 用于散列存储结构中确定对象的存储位置。可用于快速比较两个对象是否不同,因为如果它们的哈希码不同,那么它们肯定不相等。

equals 用于比较两个对象的内容是否相等,通常需要重写自定义比较逻辑。

== 用于比较两个引用是否指向同一个对象(即内存地址)。对于基本数据类型,比较它们的值。

散列表的相关类

比如 HashSet,我们常用来得到一个不重复的集合。

现在有个 Yes 类的 HashSet 集合,我只重写了 Yes 类的 equals 方法,表明如果 name 相同就返回 true。

public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj instanceof Yes) {
Yes other = (Yes) obj;
return name.equals(other.name);
}
return false;
}

就重写一个 equals 的话,HashSet 中会出现相同 name 的 Yes 对象。

原因就是 hashCode 没有重写,那为什么会这样呢?因为 HashSet 是复用 HashMap 的能力存储对象,而塞入 key 的时候要计算 hash 值,可以看到这里实际会调用对象的 hashCode 方法来计算 hash 值。

然后在具体执行 putVal 方法的时候,相关的判断条件会先判断 hash 值是否相等,如果 hash 值都不同,那就认为这两个对象不相等,这与我们之前设定的 name 一样的对象就是相等的条件就冲突了

因此规定,重写 equals 方法的时候,也要重写 hashCode 方法,这样才能保持条件判断的同步。我建议不管会不会用到散列表,只要你重写 equals 就一起重写 hashCode ,这样肯定不会出错。

Java 泛型的作用是什么?什么是泛型擦除?

泛型可以把类型当作参数一样传递,使得像一些集合类可以明确存储的对象类型,不用显示地强制转化(在没泛型之前只能是Object,然后强转)。

并且在编译期能识别类型,类型错误则会提醒,增加程序的健壮性和可读性。

泛型擦除指的是参数类型在编译之后就被抹去了,也就是生成的 class 文件是没有泛型信息的,所以称之为擦除。(在代码里写死的泛型类型是不会被擦除的)

什么是 Java 泛型的上下界限定符?

上界限定符是 extends ,下界限定符是 super

<? extends T> 表示类型的上界,?这个类型要么是 T ,要么是 T 的子类

<? super T> 表示类型的下界(也叫做超类型限定),?这个类型是 T 的超类型(父类型),直至 Object

我们在使用上下界通配符的时候,需要遵循 pecs 原则,即Producer Extends, Consumer Super;上界生产,下界消费。

什么意思呢?

如果要从集合中读取类型 T 的数据,并且不能写入,可以使用 ? extends 通配符;(Producer Extends),如上面的 processNumber 方法,我们是要从方法中得到 T 类型,也就是方法给我们生产。

如果要从集合中写入类型 T 的数据,并且不需要读取,可以使用 ? super 通配符;(Consumer Super),如上面的 addElements 方法,我们是要往方法中传入 T 类型,也就是方法帮我们消费。

Java 的 Optional 类是什么?它有什么用?

Optional 是 Java 8 引入的一个用来解决空指针异常的问题的容器类,它用来表示一个值可能存在或不存在。通过Optional类,我们可以避免在获取对象值时出现空指针异常。常见的使用方式如下:

Optional<User> userOption = Optional.ofNullable(userService.getUser(...));
if (!userOption.isPresent()) {....}

Optional 其实就是一个壳,里面放着原先的值,至于这个值是不是 null 另说,反正拿到的这个壳肯定不是 null。

我认为 Optional 的好处在于可以简化平日里一系列判断 null 的操作,使得用起来的时候看着不需要判断 null,纵享丝滑,表现出来好像用 Optional 就不需要关心空指针的情况。

而事实上是 Optional 在替我们负重前行,该有的判断它替我们完成了,而且用了 Optional 最后拿结果的时候还是小心的,盲目 get 一样会抛错。

Optional类主要有三个常用方法,分别是of()、ofNullable()和orElse()。

  • of()方法用来创建一个非空的Optional实例
  • ofNullable()方法可以创建一个可能为空的Optional实例
  • orElse()方法可以在Optional为空时提供一个默认值。

我们来看一下代码就很清楚 Optional 的好处在哪儿了。比如现在有个 yesSerivce 能 get 一个 Yes,此时需要输出 Yes 所在的省,此时的代码是这样的:

Yes yes = getYes();
if (yes != null) {
Address yesAddress = yes.getAddress();
if (yesAddress != null) {
Province province = yesAddress.getProvince();
System.out.println(province.getName());
}
}
throw new NoSuchElementException(); //如果没找到就抛错

如果用 Optional 的话,那就变成下面这样:

Optional.ofNullable(getYes())
.map(a -> a.getAddress())
.map(p -> p.getProvince())
.map(n -> n.getName())
.orElseThrow(NoSuchElementException::new);

可以看到,如果用了 Optional,代码里不需要判空的操作,即使 address 、province 为空的话,也不会产生空指针错误,这就是 Optional 带来的好处!

什么是 Java 中的迭代器(Iterator)?

迭代器(Iterator)其实是一种设计模式,用于遍历集合(例如 List、Set、Map 等)中的元素,而不需要暴露集合的内部实现,即不需要了解集合的底层结构。

在 Java 中 Iterator 是一个接口,在 java.util 包中的,常用的方法是:

  • hasNext():如果迭代器还有更多的元素可以迭代,则返回 true,否则返回 false。
  • next():返回迭代器的下一个元素。如果没有更多元素,调用该方法将抛出 NoSuchElementException。
  • remove():从底层集合中移除 next() 方法返回的上一个元素。这个方法是可选的,不是所有的实现都支持该操作。如果不支持,调用时会抛出 UnsupportedOperationException。
// 获取集合的迭代器
Iterator<String> iterator = list.iterator();

迭代器模式带来了很多好处:

  • 封装性:它将集合遍历行为和具体的实现分离,使得使用者不需要了解集合具体的内部实现。
  • 一致性:所有的集合都实现了 Iterator 接口,因此对于不同集合的遍历代码都是一致的。
  • 灵活性:因为遍历接口一致,使得可以很灵活的替换底层实现的集合而不需要改变上层的遍历代码。

Iterator 提供了单向遍历方法,如果需要支持双向遍历,可以使用 ListIterator 接口。

请解释一下 Java 8 Stream API 和 Lambda 表达式的作用,以及在项目中如何应用它们来简化集合处理?

Stream API 和 Lambda 表达式是 Java 8 提供的语法糖,它们的作用是使集合处理更加简洁、易读和高效。

Lambda 表达式

Lambda 表达式是一种匿名函数,允许你以更紧凑的方式传递代码块,简化代码的编写,比如:

// 使用 Lambda 表达式过滤以 "A" 开头的名字
List<String> filteredNames = names.stream()
.filter(name -> name.startsWith("A"))
.collect(Collectors.toList());

Stream 流

Stream API 是一种流式操作集合的方法,提供了丰富的集合处理操作,比如过滤、映射、排序等,还支持延迟加载和并行处理,简化代码、并提高编码效率。

上述代码示例同样也是 Stream API 的应用。

在本项目中,匹配相似用户时,将存储用户和匹配度的 List 转化为 Stream 流,用 sorted 方法进行编辑距离由小到大排序,用 limit 方法取流的前 N 项,用 collect 终结操作将处理好的流转化为集合 List。还有使用 Stream API 的 map 方法对用户列表中的每个用户信息进行脱敏、使用ParallelStream 实现并发流等。