申明:本人于公众号Java筑基期,CSDN先后发当前文章,标明原创,转载二次发文请注明转载公众号,另外请不要再标原创 ,注意违规
在Java中,共有八种基本数据类型,它们分别是:
byte:字节型,占用8位,取值范围为 -128 到 127。
short:短整型,占用16位,取值范围为 -32,768 到 32,767。
int:整型,占用32位,取值范围为 -2,147,483,648 到 2,147,483,647。
long:长整型,占用64位,取值范围为 -9,223,372,036,854,775,808 到 9,223,372,036,854,775,807。
float:单精度浮点型,占用32位,可以表示带小数点的数值。
double:双精度浮点型,占用64位,可以表示更大范围的带小数点的数值。
char:字符型,占用16位,用于表示单个字符,如 ‘A’、‘b’、‘1’ 等。
boolean:布尔型,用于表示逻辑值,只有两个取值:true 和 false。
这些基本数据类型是构建Java程序的基础,它们可以用于声明变量、存储数据和执行各种计算操作。在使用基本数据类型时,需要注意其取值范围和所占用的内存大小,以确保数据的准确性和程序的性能。
看到这里,细心的人肯定会问,为什么有两个浮点型,单精度和双精度又是什么意思?
大家都知道浮点型是一种用于表示带小数点的数值的数据类型。在计算机中,浮点数用于存储实数,即包含整数部分和小数部分的数值。
单精度和双精度是浮点型的两种表示方式,它们分别使用32位和64位存储空间。
讲到这个,我们就要讲讲实习面试时经常会被问到的一个问题了,为什么不能用浮点型表示金额?
不要小瞧了它,在金融行业,无论几年工作经验,这个行业是一定会问的,生怕刚好面了一个不知道的。
这是因为我们使用浮点型表示金额会涉及到精度丢失的问题。
因为浮点数的表示方式采用二进制表示,某些十进制数在二进制表示中是无限循环的,这会导致精度损失。
所以在进行金融计算等要求精确计算的场景中,精确的小数点后几位是非常重要的,而浮点数的精度问题可能会导致计算结果出现不可预测的误差。
自动拆装箱是Java中的一种特性,用于在基本数据类型(如int、float等)和对应的包装类型(如Integer、Float等)之间进行自动的转换。自动拆装箱是Java中的一种特性,用于在基本数据类型(如int、float等)和对应的包装类型(如Integer、Float等)之间进行自动的转换。
用代码来说话吧:
场景1:集合类的使用 在Java的集合类中,通常只能存储对象类型(即包装类型),而不能存储基本类型。如果我们需要将一组整数存储在ArrayList中,就需要使用Integer这样的包装类型。
public static void main(String[] args) {
// 使用基本类型int数组
int[] intArray = {1, 2, 3, 4, 5};
// 使用包装类型Integer集合
ArrayList<Integer> integerList = new ArrayList<>();
for (int num : intArray) {
integerList.add(num); // 自动装箱
}
// 从集合中取出数据并进行计算
int sum = 0;
for (Integer num : integerList) {
sum += num; // 自动拆箱
}
System.out.println("Sum: " + sum);
}
场景2:使用泛型 在使用泛型时,如果要表示一个未知类型的数值,需要使用包装类型作为泛型参数。
public static void main(String[] args) {
// 使用包装类型Integer作为泛型参数
ArrayList<Integer> myList = new ArrayList<>();
myList.add(10); // 自动装箱
myList.add(20);
myList.add(30);
int sum = 0;
for (Integer num : myList) {
sum += num; // 自动拆箱
}
System.out.println("Sum: " + sum);
}
因此,在需要使用对象的场景下,应该选择包装类型;而在需要高效的数值计算和内存占用较小的场景下,可以选择基本类型。在Java 5及以上版本中,由于引入了自动拆装箱特性,基本类型和包装类型之间的转换会更加方便和自然,开发者不再需要过多关注这些转换的细节。
说到包装类型,用的最多的就是String,就来讲讲关于它最经典的面试题吧:
关于String字符串的不可变性:
在Java中,String是一种不可变的类,即一旦创建了String对象,它的值就不能被修改。这种不可变性是通过以下几个特性来实现的:
不可变性带来了以下好处:
尽管String本身是不可变的,但我们可以通过创建新的String对象或使用StringBuilder或StringBuffer类来对字符串进行修改或拼接。不可变性是String类设计的核心特点之一,它在Java中有着广泛的应用,例如在字符串处理、缓存、哈希表等方面。
JDK 6和JDK 7中substring的原理及区别
关于String的用法,除了String.valueOf()以外,我用的最多的就是substring()了吧。
在 JDK 6 中,substring 方法会创建一个新的字符串对象,该对象包含原始字符串中指定索引范围的字符。例如,调用 "Hello World".substring(0, 5)
将返回一个新的字符串对象 "Hello"
。
public String substring(int beginIndex, int endIndex) {
// 参数合法性检查
if (beginIndex < 0) {
throw new StringIndexOutOfBoundsException(beginIndex);
}
if (endIndex > value.length) {
throw new StringIndexOutOfBoundsException(endIndex);
}
int subLen = endIndex - beginIndex;
if (subLen < 0) {
throw new StringIndexOutOfBoundsException(subLen);
}
// 创建新的字符串对象,复制字符数据
return ((beginIndex == 0) && (endIndex == value.length)) ? this :
new String(value, beginIndex, subLen);
}
public String(char value[], int offset, int count) {
this.value = value;
this.offset = offset;
this.count = count;
}
**面试装逼的说:**在JDK 6的实现中,substring 方法通过复制原始字符串中的字符来创建新的字符串对象。这意味着新字符串和原始字符串共享同样的字符数组,即使新字符串只是原始字符串的一部分,整个字符数组仍然被保留在内存中。
**面试简单的说:**在JDK 6 的实现中,在创建新的字符串对象时,使用了 new String(...)
来复制字符数据,即创建了一个新的字符数组来保存子字符串的内容。
而在 JDK 7 中,substring 方法的实现发生了改变。它不再创建新的字符数组来保存子字符串,而是将原始字符串的字符数组直接引用到新的字符串对象中。这意味着在 JDK 7 中,当调用 substring 方法时,新的字符串对象与原始字符串共享相同的字符数组,不再复制字符数据,从而节省了内存开销。
public String substring(int beginIndex, int endIndex) {
// 参数合法性检查
if (beginIndex < 0) {
throw new StringIndexOutOfBoundsException(beginIndex);
}
if (endIndex > value.length) {
throw new StringIndexOutOfBoundsException(endIndex);
}
int subLen = endIndex - beginIndex;
if (subLen < 0) {
throw new StringIndexOutOfBoundsException(subLen);
}
// 创建新的字符串对象,共享字符数组
return ((beginIndex == 0) && (endIndex == value.length)) ? this :
new String(value, beginIndex, subLen);
}
public String(char value[], int offset, int count){
.....
this.value = Arrays.copyOfRange(value, offset, offset+count);
}
**面试装逼的说:**和 JDK 6 的实现类似,JDK 7 的 substring
方法也先进行参数合法性检查,确保传入的索引值在合法范围内。然后计算子字符串的长度 subLen
。在 JDK 7 的实现中,当 beginIndex
为 0 且 endIndex
等于原始字符串的长度时,表示要获取的子字符串与原始字符串完全相同,此时直接返回原始字符串本身(即 this
),而不再创建新的字符串对象。
**面试简单的说:**JDK 7 的实现中,在创建新的字符串对象时,同样使用了 new String(...)
,但当 beginIndex
和 endIndex
指定的子字符串与原始字符串不同的情况下,它将共享相同的字符数组,不再复制字符数据。
字符串拼接的⼏种⽅式和区别
一般我会讲4种:
使用"+"运算符拼接:
public static void main(String[] args) {
String str1 = "Hello";
String str2 = " World";
String result = str1 + str2;
System.out.println(result);
}
这是最简单的字符串拼接方式,使用"+“运算符可以将两个字符串连接成一个新的字符串。然而,当需要拼接多个字符串时,使用”+"运算符会生成大量的临时中间字符串,效率较低。
使用StringBuilder拼接:
public static void main(String[] args) {
StringBuilder sb = new StringBuilder();
sb.append("Hello");
sb.append(" World");
String result = sb.toString();
System.out.println(result);
}
StringBuilder类是专门用于字符串拼接的可变字符序列,它的append方法可以高效地在末尾添加字符串。在需要拼接大量字符串时,使用StringBuilder比使用"+"运算符要高效,因为它避免了创建大量的临时字符串。
使用StringBuffer拼接:
public static void main(String[] args) {
StringBuffer sb = new StringBuffer();
sb.append("Hello");
sb.append(" World");
String result = sb.toString();
System.out.println(result);
}
StringBuffer与StringBuilder类似,也是可变字符序列,但它是线程安全的。如果在多线程环境下进行字符串拼接,推荐使用StringBuffer,但在单线程情况下,StringBuilder通常性能更好。
使用String的concat方法:
public static void main(String[] args) {
String str1 = "Hello";
String str2 = " World";
String result = str1.concat(str2);
System.out.println(result);
}
String类提供了concat方法,用于连接两个字符串。和"+"运算符一样,这种方法也会创建大量临时中间字符串,效率较低。
四种的区别:
**面试简单的说:**对于频繁拼接大量字符串的情况,会使用StringBuilder或StringBuffer来实现,以提高性能和效率。
关于关键字,最为主要的是避免将这些关键字用作标识符(如变量名、方法名等),以免引起编译错误。
以下是一些常见的Java关键字:
class
: 定义类。interface
: 定义接口。extends
: 继承一个类或实现一个接口。implements
: 实现接口。public
, private
, protected
: 访问修饰符,用于控制类、方法和属性的访问权限。static
: 用于定义静态方法、静态变量或静态代码块。final
: 常量修饰符,用于表示一个不可修改的值或不可继承的类。abstract
: 抽象类或抽象方法修饰符,用于表示类或方法是抽象的,不能直接实例化。new
: 创建新对象的关键字。this
: 表示当前对象的引用。super
: 表示父类对象的引用。if
, else
: 条件语句关键字。for
, while
, do
: 循环语句关键字。switch
, case
, default
: 选择语句关键字。return
: 从方法中返回值的关键字。try
, catch
, finally
: 异常处理关键字。关于集合类,我们最常见也是用的最多的无非List和Set这两种
List和Set的区别
主要区别:
Set如何保证元素不重复
Set接口在实现时,对于添加元素时会根据值来判断是否已经存在相同元素,从而保证元素不重复。准确的说是使用了元素的hashCode方法来计算哈希值,然后寻找是否已经添加了相同哈希值的元素。
Collection和Collections区别
Collection作为Java中表示集合的接口(interface),它同时也支持泛型Collection,它是List、Set和Queue接口的父接口,定义了集合的基本操作和行为。
Collections是Java中的一个实用类,它提供了一系列静态方法来操作集合,例如对集合进行排序、查找最大最小值、反转集合等。
在Java中,集合类提供了多种遍历方式,可以用来遍历集合中的元素。
List<String> list = new ArrayList<>();
list.add("Apple");
list.add("Banana");
list.add("Orange");
假设有上面一个名为list的集合,以下是常见的集合遍历方式:
使用Iterator遍历
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String element = iterator.next();
System.out.println(element);
}
使用for-each循环遍历(增强for循环)
for (String element : list) {
System.out.println(element);
}
使用普通for循环遍历(适用于List)
for (int i = 0; i < list.size(); i++) {
String element = list.get(i);
System.out.println(element);
}
使用forEach方法(适用于Java 8及以上版本)
list.forEach(element -> System.out.println(element));
注意事项:
面试装逼的说:
ArrayList、LinkedList和Vector都是Java中的集合类,用于存储一组对象。它们有以下区别:
面试简单的说:
背起来可能有点困难,我再总结,缩减一下:
Emm…或许还可以这样:
不能再缩了…
面试装逼的说:
HashSet、LinkedHashSet和TreeSet都是Java中的Set接口的实现类,用于存储一组不重复的元素。它们之间的主要区别如下:
面试简单的说:
再总结,缩减一下:
再精华一下就是:
面试装逼的说:
HashMap、HashTable和ConcurrentHashMap都是Java中用于存储键值对的Map接口的实现类。
但它们在线程安全性、同步方式和性能方面有一些区别:
面试简单的说:
试着总结,缩减一下:
再精华一下就是:
看到这道题就有阴影,因为经常被问
大家都知道HashMap是Java中常用的哈希表(散列表)实现。
面试装逼的说:
它是通过数组和链表/红黑树组合的方式来存储键值对,以实现高效的数据存储和查找。而且在HashMap中,键和值都可以为null,且不保证元素的插入顺序。
数据结构:
扩容机制: HashMap在插入键值对时,会根据其哈希值计算其在数组中的索引位置,然后将键值对存储在该位置。当元素数量达到一定阈值(负载因子)时,HashMap会触发扩容操作,以保持数组的填充因子不超过预设值,从而保持较好的查找性能。
扩容操作:
扩容的过程会比较耗时,但是通过扩容可以保证HashMap在不断增加元素时,仍能保持较好的查找性能。负载因子是影响扩容触发的重要因素,一般情况下,当HashMap中的元素数量达到容量乘以负载因子时,就会触发扩容操作,默认负载因子为0.75,这也是JDK中HashMap的默认值。
面试简单的说(说真的简单不起来):
总结一下:
在HashMap中,size和capacity是两个不同的概念,甚至用得少的只知道Size,而不知Capacity,拿下面的这份代码来举例:
HashMap<String, Integer> hashMap = new HashMap<>(16);
hashMap.put("apple", 1);
hashMap.put("banana", 2);
hashMap.put("orange", 3);
Size(大小):
大家都知道因为已经存储了三个键值对在里面了,所以当前hashMap的size是3。
Capacity(容量):
而Capacity的大小是16,表示HashMap内部数组的长度,即HashMap能够容纳键值对的槽数量。一般这个大小会是2的幂次方,如16、32、64等等。且在HashMap的实现中,capacity会随着元素的增加而动态改变。当元素数量达到负载因子(默认为0.75)乘以容量时,HashMap会触发扩容操作,将capacity翻倍,以保持较好的性能。扩容后,HashMap会重新计算哈希值,并将元素重新分配到新的更大数组中。
总结:
size()
方法获取。capacity()
方法获取。在HashMap中,loadFactor(负载因子)和threshold(阈值)是用于控制HashMap扩容的重要参数
LoadFactor(负载因子):
负载因子是一个表示HashMap在什么时候进行扩容的系数。它是HashMap中实际元素数量与容器大小(数组长度)的比率。默认情况下,负载因子是0.75。
公式:负载因子 = 实际元素数量 / 容器大小
当实际元素数量达到负载因子乘以容器大小时,即 size >= loadFactor * capacity
,HashMap会触发扩容操作。负载因子越大,意味着HashMap在容器未满的情况下就会进行扩容,减少哈希冲突的可能性。但同时,较大的负载因子会增加空间的浪费。负载因子较小时,HashMap需要更频繁地进行扩容,但空间利用率更高。
Threshold(阈值):
阈值是实际元素数量超过多少时,HashMap会进行扩容的具体阈值。它是负载因子乘以容器大小,即 threshold = loadFactor * capacity
。
当HashMap中的元素数量达到阈值时,会触发扩容操作,将HashMap的容器大小翻倍,以保持较好的性能。新的容器大小为原容器大小的两倍。
公式:阈值 = 负载因子 * 容器大小
总结:通过调整负载因子和容器大小,可以在空间利用率和性能之间进行权衡。通常情况下,默认的负载因子0.75是一个较为合理的选择,同时也建议初始化HashMap时指定容器大小,以避免过多的扩容操作。
常常会看到提示,建议我们new HashMap()时,为HashMap指定容量,比如刚刚的:
HashMap<String, Integer> hashMap = new HashMap<>(16);
面试简单的说:
主要原因是为了提高性能和避免不必要的扩容操作。
面试复杂的说:
而且,容量的设置并不会影响集合的逻辑功能,只是在性能和内存利用方面有所优化。过小的容量可能导致频繁的扩容,过大的容量可能会浪费内存,因此建议根据预期的元素数量来合理设置集合的容量。
通过4.8近而衍生出另一个问题
大家都知道…咳咳,这道是送命题,为啥,因为我送过。这道题,如果你回答出一个值,任何值都是错误的。
因为初始容量设置成多少合适,取决于你对元素数量的预估和性能需求。一般情况下,使用你预计的值,除以默认的负载因子0.75,所得的值作为初始容量最合适。比如我认为这个功能大概会有100个数据,那么100/0.75=133,那么我就会创建:
HashMap<String, Integer> hashMap = new HashMap<>(133);
使用133作为Capacity的值。
面试简单的说:
使用预计的总数值,除以默认的负载因子0.75,所得的数值作为初始容量最合适,然后根据具体场景和数据量的预估,再适当进行调整,减少扩容的次数。
大家都知道默认负载因⼦为0.75,有很多人好奇,为什么默认负载因⼦会设置为0.75,这个跟内存利用率、性能、哈希冲突都有关系:
面试简单的说:
背不下的背这个:
0.75的负载因子在大多数场景下能够提供较好的性能和空间利用率。
大家都知道HashMap是非线程安全的容器,尤其是在多线程的情况下,对同一个HashMap对象进行插入、删除、修改操作时,会导致数据不一致和丢失问题。
对策就是使用ConcurrentHashMap,估计差不多是所有人都会背的一个了。
但是其实,你也可以用另外一种方式,这种面试官也会经常问,如果不用ConcurrentHashMap,你该怎么办?讲不出来?那你就GG了。
第一种:
Collections.synchronizedMap
获取同步mapMap<String, Integer> synchronizedMap = Collections.synchronizedMap(new HashMap<>());
第二种:
synchronized
Map<String, Integer> hashMap = new HashMap<>();
synchronized (hashMap) {
// 对hashMap进行操作
}
当然了,装逼归装逼,为了保证多线程下的hashmap的数据一致性,我还是会乖乖使用ConcurrentHashMap,并且ConcurrentHashMap的效率也比较高。别问我为什么,因为只要你有过生产环境出问题,客户在现场参观,甲方一直催的经历的话,你也会乖乖使用ConcurrentHashMap。
大家应该都遇过,面试关于HashMap的时候,总会问你转红黑树的阈值问题。
在JDK 8中,Java对HashMap进行了一些优化,其中一个改进是引入了红黑树来优化链表过长的问题。当链表长度为8时,HashMap会将链表转成红黑树,以便提高查找和插入的性能。
别问为什么?这是开发者经过实验和性能测试得出的一个经验值,链表长度达到8的概率较低,此时转换为红黑树可以显著减少查找元素的时间复杂度,从而提高HashMap的性能。
链表长度较小时,使用链表进行查找是较为高效的。因为链表进行插入和查找的时间复杂度都是O(1),但是一旦链表过长,查找时间会变成O(n),n为长度。而红黑树查找的时间复杂度为O(log n),明显时间较短,性能更高。但是如果使用红黑树存储较少数据的时候,红黑树额外的开销会影响性能。红黑树相对于链表来说,存储每个节点需要更多的内存空间,因为红黑树需要维护额外的颜色信息、左右子节点指针等。在元素较少的情况下,使用红黑树可能会占用更多的内存。且插入和删除还需要进行自平衡调整,这些都是额外开销。因此,将链表转换为红黑树是在平衡链表长度和查找性能之间进行的权衡,而8作为阈值在大多数场景中表现较好。
总结一下在JDK 8中HashMap将链表转换为红黑树的阈值设置为8的原因:
总体来说,将链表转换为红黑树的阈值设置为8在大多数场景中表现较好,能够在提高查找性能的同时,不引入过多的额外开销。不过,具体的阈值设置也可以根据实际应用场景进行调整。HashMap在JDK 8中的这一改进使其在大规模数据和高并发场景下的性能得到显著提升。