做网站电脑配置要求个高吗,电子商务网站的建设包含哪些流程图,drupal网站建设,seo自动优化工具申明#xff1a;本人于公众号Java筑基期#xff0c;CSDN先后发当前文章#xff0c;标明原创#xff0c;转载二次发文请注明转载公众号#xff0c;另外请不要再标原创 #xff0c;注意违规 Java基础知识
1、基本数据类型
在Java中#xff0c;共有八种基本数据类型… 申明本人于公众号Java筑基期CSDN先后发当前文章标明原创转载二次发文请注明转载公众号另外请不要再标原创 注意违规 Java基础知识
1、基本数据类型
在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程序的基础它们可以用于声明变量、存储数据和执行各种计算操作。在使用基本数据类型时需要注意其取值范围和所占用的内存大小以确保数据的准确性和程序的性能。
1.1 讲讲面试题
看到这里细心的人肯定会问为什么有两个浮点型单精度和双精度又是什么意思
大家都知道浮点型是一种用于表示带小数点的数值的数据类型。在计算机中浮点数用于存储实数即包含整数部分和小数部分的数值。
单精度和双精度是浮点型的两种表示方式它们分别使用32位和64位存储空间。
单精度浮点型float占用32位其中1位用于表示符号8位用于指数剩下的23位用于尾数。单精度浮点数可以表示大约6到7位有效数字其表示范围大约在-3.4E38到3.4E38之间。双精度浮点型double占用64位其中1位用于表示符号11位用于指数剩下的52位用于尾数。双精度浮点数可以表示大约15到16位有效数字其表示范围大约在-1.7E308到1.7E308之间。
讲到这个我们就要讲讲实习面试时经常会被问到的一个问题了为什么不能用浮点型表示金额
不要小瞧了它在金融行业无论几年工作经验这个行业是一定会问的生怕刚好面了一个不知道的。
这是因为我们使用浮点型表示金额会涉及到精度丢失的问题。
因为浮点数的表示方式采用二进制表示某些十进制数在二进制表示中是无限循环的这会导致精度损失。
所以在进行金融计算等要求精确计算的场景中精确的小数点后几位是非常重要的而浮点数的精度问题可能会导致计算结果出现不可预测的误差。
2、自动拆装箱
自动拆装箱是Java中的一种特性用于在基本数据类型如int、float等和对应的包装类型如Integer、Float等之间进行自动的转换。自动拆装箱是Java中的一种特性用于在基本数据类型如int、float等和对应的包装类型如Integer、Float等之间进行自动的转换。
基本类型和包装类型 基本类型指的是Java中最原始的数据类型包括byte、short、int、long、float、double、char和boolean。这些类型在内存中直接存储数值不具备对象的特性也没有方法可以调用。包装类型Java为每个基本类型提供了对应的引用类型称为包装类型。例如int对应的包装类型是Integerdouble对应的包装类型是Double。包装类型是对象具有一些附加的功能比如可以调用方法支持泛型等。 自动拆装箱 自动拆箱Unboxing指的是将包装类型自动转换为对应的基本类型。例如当我们将一个Integer对象赋值给int类型的变量时会自动将Integer对象的值拆箱为int类型的值。自动装箱Autoboxing指的是将基本类型自动转换为对应的包装类型。例如当我们将int类型的值赋值给一个Integer类型的变量时会自动将int值装箱为Integer对象。 整形缓存机制 在Java中对于范围在-128到127之间的byte、short、int和char类型的数值会被缓存起来以便重复使用。这意味着当我们创建这些数据类型的对象时如果值在-128到127之间将不会每次都创建新的对象而是直接使用缓存中的对象从而节省了内存开销。 基本类型和包装类型选择的场景 使用基本类型的优点是它们更高效占用内存更少因为它们是直接存储数值的。使用包装类型的优点是它们是对象可以在需要对象的场景中使用比如集合类、泛型、反射等。同时包装类型提供了一些有用的方法和功能比如可以转换为字符串、进行数值操作等。
用代码来说话吧
场景1集合类的使用 在Java的集合类中通常只能存储对象类型即包装类型而不能存储基本类型。如果我们需要将一组整数存储在ArrayList中就需要使用Integer这样的包装类型。
public static void main(String[] args) {// 使用基本类型int数组int[] intArray {1, 2, 3, 4, 5};// 使用包装类型Integer集合ArrayListInteger 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作为泛型参数ArrayListInteger 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及以上版本中由于引入了自动拆装箱特性基本类型和包装类型之间的转换会更加方便和自然开发者不再需要过多关注这些转换的细节。
2.1 讲讲面试题
说到包装类型用的最多的就是String就来讲讲关于它最经典的面试题吧
关于String字符串的不可变性:
在Java中String是一种不可变的类即一旦创建了String对象它的值就不能被修改。这种不可变性是通过以下几个特性来实现的
String类使用final关键字修饰String类被声明为final类意味着它不能被继承防止子类对其进行修改。字符串存储在常量池中Java中的字符串常量例如“Hello”都是存储在一个被称为常量池String Pool的特殊区域内。当创建一个新的字符串时如果常量池中已经存在相同内容的字符串则直接返回常量池中的引用而不是创建新的对象。字符串的值不可变String类中的字符数组被声明为private final char[] value这使得String对象内部的字符数组不能被外部修改。
不可变性带来了以下好处
线程安全由于字符串是不可变的多个线程可以安全地共享一个字符串对象而不需要担心数据被修改的问题。缓存Hash值由于字符串的不可变性String的hashCode()方法可以在第一次计算后缓存该值加快了哈希表等数据结构的性能。安全性不可变性确保字符串在传递过程中不会被意外修改从而增加代码的可靠性和安全性。
尽管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, offsetcount);
}**面试装逼的说**和 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拼接字符串时可以避免产生大量的临时中间字符串效率较高适合拼接大量字符串。使用String的concat方法拼接字符串时效率与运算符类似也会产生大量的临时中间字符串不适合拼接大量字符串。 **面试简单的说**对于频繁拼接大量字符串的情况会使用StringBuilder或StringBuffer来实现以提高性能和效率。
3、关键字
关于关键字最为主要的是避免将这些关键字用作标识符如变量名、方法名等以免引起编译错误。
以下是一些常见的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: 异常处理关键字。
4、集合类
关于集合类我们最常见也是用的最多的无非List和Set这两种 List和Set的区别 List也是Java中的一个接口同样继承自Collection接口用于表示一个有序的、可重复的元素集合。Set是Java中的一个接口它继承自Collection接口用于表示一组不重复的元素不允许包含重复的元素。 主要区别 List中的元素是有序的会保持插入的顺序因此可以通过索引来访问元素。List允许包含重复的元素即相同的元素可以在List中出现多次。Set中的元素是无序的不会保持插入顺序因此不能通过索引来访问元素。Set不允许包含重复的元素即相同的元素只会在Set中保留一个。 Set如何保证元素不重复 Set接口在实现时对于添加元素时会根据值来判断是否已经存在相同元素从而保证元素不重复。准确的说是使用了元素的hashCode方法来计算哈希值然后寻找是否已经添加了相同哈希值的元素。 Collection和Collections区别 Collection作为Java中表示集合的接口(interface)它同时也支持泛型Collection它是List、Set和Queue接口的父接口定义了集合的基本操作和行为。 Collections是Java中的一个实用类它提供了一系列静态方法来操作集合例如对集合进行排序、查找最大最小值、反转集合等。
4.1 集合的遍历
在Java中集合类提供了多种遍历方式可以用来遍历集合中的元素。
ListString list new ArrayList();
list.add(Apple);
list.add(Banana);
list.add(Orange);假设有上面一个名为list的集合以下是常见的集合遍历方式 使用Iterator遍历 IteratorString 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));注意事项
遍历Set时由于Set是无序的所以无法使用普通for循环遍历。只能使用Iterator、for-each循环或forEach方法。遍历List时推荐使用for-each循环或forEach方法因为它们更简洁、易读并且不需要手动维护索引变量。在遍历过程中如果需要在循环内部对集合进行增加、删除等操作请使用Iterator并使用Iterator的remove方法进行安全的操作。
4.2 ArrayList和LinkedList和Vector的区别
面试装逼的说
ArrayList、LinkedList和Vector都是Java中的集合类用于存储一组对象。它们有以下区别
实现方式 ArrayList是基于动态数组实现的内部使用数组来存储元素可以动态扩容和缩容。LinkedList是基于链表实现的内部使用双向链表来存储元素。Vector也是基于动态数组实现的和ArrayList类似但是它是线程安全的支持同步操作。 线程安全性 ArrayList和LinkedList是非线程安全的不支持多线程并发操作如果需要在多线程环境下使用需要手动进行同步处理。Vector是线程安全的支持多线程并发操作内部的方法都使用了synchronized关键字进行同步因此相对于ArrayList和LinkedListVector的性能较差。 遍历性能 ArrayList由于基于数组因此在随机访问元素时效率较高时间复杂度为O(1)。但在插入或删除元素时需要进行数组的复制和移动效率较低时间复杂度为O(n)。LinkedList在插入或删除元素时效率较高因为只需要改变链表节点的指针指向时间复杂度为O(1)。但在随机访问元素时效率较低需要从头节点或尾节点开始遍历时间复杂度为O(n)。Vector的性能和ArrayList类似在随机访问元素时效率较高但在插入或删除元素时效率较低。 空间占用 ArrayList和Vector都是基于动态数组实现的因此会预先分配一定的空间如果元素数量超过了初始分配的空间需要重新分配更大的空间会导致内存浪费。LinkedList不需要预先分配连续的内存空间每个元素通过链表节点链接因此空间利用率较高。
面试简单的说
背起来可能有点困难我再总结缩减一下
ArrayList 基于动态数组实现支持随机访问元素时间复杂度为 O(1)。插入和删除元素时需要进行数组的复制和移动效率较低时间复杂度为 O(n)。不是线程安全的适合在单线程环境中使用。 LinkedList 基于双向链表实现插入和删除元素时效率较高时间复杂度为 O(1)。随机访问元素时效率较低时间复杂度为 O(n)。不是线程安全的适合在单线程环境中使用。 Vector 基于动态数组实现类似于 ArrayList支持随机访问元素。所有方法都使用 synchronized 关键字进行同步因此是线程安全的。由于同步操作性能较低在多线程环境下才使用。
Emm…或许还可以这样
如果需要高效的随机访问ArrayList是较好的选择。如果需要频繁的插入和删除操作LinkedList可以更高效地执行。而Vector适用于需要线程安全的情况但在性能上相对较差通常只在多线程环境下使用。
不能再缩了…
4.3 HashSet、LinkedHashSet和 TreeSet的区别
面试装逼的说
HashSet、LinkedHashSet和TreeSet都是Java中的Set接口的实现类用于存储一组不重复的元素。它们之间的主要区别如下
HashSet HashSet是基于哈希表实现的不保证元素的顺序也不允许包含重复的元素。在HashSet中添加元素的顺序可能与它们被存储的顺序不同因为HashSet使用哈希值来存储和获取元素哈希值不会按照元素的插入顺序排列。 LinkedHashSet LinkedHashSet是HashSet的子类在HashSet的基础上增加了维护元素插入顺序的功能。LinkedHashSet通过双向链表来维护元素的插入顺序因此在遍历时会按照元素插入的顺序进行输出。 TreeSet TreeSet是基于红黑树自平衡二叉查找树实现的它会对元素进行排序并保持排序状态。TreeSet中的元素是有序的按照元素的自然顺序或指定的比较器顺序进行排列。因此对于元素类型为基本数据类型或实现了Comparable接口的类默认按照元素自然顺序进行排序。对于没有实现Comparable接口的类可以通过传入一个Comparator来指定排序方式。
面试简单的说
再总结缩减一下
HashSet是最基本的集合实现类无序且不允许重复元素。LinkedHashSet在HashSet的基础上维护元素插入顺序保持元素的插入顺序。TreeSet在HashSet的基础上使用红黑树来对元素进行排序并保持排序状态。
再精华一下就是
如果需要无序的、不重复的集合可以使用HashSet。如果需要按照插入顺序来遍历集合可以使用LinkedHashSet。如果需要有序的集合可以使用TreeSet并可以通过Comparator来自定义排序方式。
4.4 HashMap、HashTable、ConcurrentHashMap区别
面试装逼的说
HashMap、HashTable和ConcurrentHashMap都是Java中用于存储键值对的Map接口的实现类。
但它们在线程安全性、同步方式和性能方面有一些区别
HashMap HashMap是非线程安全的不适用于多线程环境。HashMap允许使用null作为键和值。HashMap在插入、查找和删除操作上具有较好的性能因为它不需要进行同步。 HashTable HashTable是线程安全的所有方法都使用 synchronized 关键字进行同步保证了多线程并发安全。HashTable不允许使用null作为键和值任何null值都会抛出NullPointerException。HashTable的性能较差因为在所有方法上都使用了同步锁即使是单线程环境下也会有性能损失。 ConcurrentHashMap ConcurrentHashMap是线程安全的通过分段锁Segment实现可以支持多个线程并发访问不同的段从而提高并发性能。ConcurrentHashMap允许使用null作为键或值但在键和值都不能为null的情况下效率会更高。ConcurrentHashMap在高并发环境下具有较好的性能相比HashTable它提供更好的并发处理能力适用于高度并发的场景。
面试简单的说
试着总结缩减一下
HashMap是非线程安全的适合在单线程环境下使用。HashTable是线程安全的但性能较差多线程环境下使用。ConcurrentHashMap是线程安全的且具有较好的并发性能适合高并发环境。
再精华一下就是
如果在多线程环境下需要进行Map操作推荐使用ConcurrentHashMap以保证线程安全和高性能。在单线程环境下可以使用HashMap来获得更好的性能。而HashTable在现代Java应用中较少使用因为它的性能相对较差通常可以用ConcurrentHashMap来替代。
4.5 HashMap 的数据结构与扩容机制
看到这道题就有阴影因为经常被问
大家都知道HashMap是Java中常用的哈希表散列表实现。
面试装逼的说
它是通过数组和链表/红黑树组合的方式来存储键值对以实现高效的数据存储和查找。而且在HashMap中键和值都可以为null且不保证元素的插入顺序。
数据结构
数组HashMap内部使用一个数组来存储键值对。数组的每个元素都是一个链表的头节点JDK 7中使用的是单向链表JDK 8中引入了红黑树优化。链表/红黑树当多个键哈希到同一个数组索引位置时它们会以链表/红黑树的形式存储在该位置。链表适用于小规模冲突红黑树用于解决大规模冲突JDK 8中引入。
扩容机制 HashMap在插入键值对时会根据其哈希值计算其在数组中的索引位置然后将键值对存储在该位置。当元素数量达到一定阈值负载因子时HashMap会触发扩容操作以保持数组的填充因子不超过预设值从而保持较好的查找性能。
扩容操作
创建新数组当HashMap需要扩容时会创建一个新的数组其长度是原数组的两倍。重新计算哈希值所有原有的键值对会根据新数组的长度进行重新计算哈希值并放入新的数组中对应的位置。处理冲突由于哈希值重新计算后原本哈希值不同但新的哈希值相同的键值对会出现在同一个数组位置上因此需要解决冲突。这里会涉及链表转换成红黑树JDK 8引入等操作。将新数组代替旧数组扩容完成后新的数组会代替旧的数组这样HashMap就完成了扩容操作。
扩容的过程会比较耗时但是通过扩容可以保证HashMap在不断增加元素时仍能保持较好的查找性能。负载因子是影响扩容触发的重要因素一般情况下当HashMap中的元素数量达到容量乘以负载因子时就会触发扩容操作默认负载因子为0.75这也是JDK中HashMap的默认值。
面试简单的说(说真的简单不起来)
总结一下
HashMap使用数组和链表/红黑树的结合来实现高效的数据存储和查找。在插入键值对时通过哈希值计算键的索引位置并将键值对存储在该位置。当元素数量达到负载因子默认为0.75乘以容量时HashMap会触发扩容操作即创建新数组、重新计算哈希值、解决冲突最终将新数组代替旧数组完成扩容。这样可以保持较好的查找性能并提高HashMap的效率和性能。但是需要注意的是尽管HashMap在单个操作上是线程安全的但在多线程环境下仍需谨慎特别是在扩容过程中可能导致链表或红黑树出现环形结构的问题。为了在多线程环境中保证安全使用可以使用ConcurrentHashMap或通过显式的同步措施来进行保护。
4.6 HashMap 中 size 和 capacity 的区别
在HashMap中size和capacity是两个不同的概念甚至用得少的只知道Size而不知Capacity拿下面的这份代码来举例
HashMapString, 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是指HashMap中当前存储的键值对的数量可以通过size()方法获取。Capacity是指HashMap内部数组的长度总是2的幂次方可以通过capacity()方法获取。Capacity会随着元素的增加而动态改变触发扩容操作以保持较好的性能。
4.7 loadFactor负载因子和threshold阈值
在HashMap中loadFactor负载因子和threshold阈值是用于控制HashMap扩容的重要参数 LoadFactor负载因子 负载因子是一个表示HashMap在什么时候进行扩容的系数。它是HashMap中实际元素数量与容器大小数组长度的比率。默认情况下负载因子是0.75。 公式负载因子 实际元素数量 / 容器大小 当实际元素数量达到负载因子乘以容器大小时即 size loadFactor * capacityHashMap会触发扩容操作。负载因子越大意味着HashMap在容器未满的情况下就会进行扩容减少哈希冲突的可能性。但同时较大的负载因子会增加空间的浪费。负载因子较小时HashMap需要更频繁地进行扩容但空间利用率更高。 Threshold阈值 阈值是实际元素数量超过多少时HashMap会进行扩容的具体阈值。它是负载因子乘以容器大小即 threshold loadFactor * capacity。 当HashMap中的元素数量达到阈值时会触发扩容操作将HashMap的容器大小翻倍以保持较好的性能。新的容器大小为原容器大小的两倍。 公式阈值 负载因子 * 容器大小
总结通过调整负载因子和容器大小可以在空间利用率和性能之间进行权衡。通常情况下默认的负载因子0.75是一个较为合理的选择同时也建议初始化HashMap时指定容器大小以避免过多的扩容操作。
4.8 指定容量
常常会看到提示建议我们new HashMap()时为HashMap指定容量比如刚刚的
HashMapString, Integer hashMap new HashMap(16);面试简单的说
主要原因是为了提高性能和避免不必要的扩容操作。
面试复杂的说
提高性能 在创建集合时如果知道预期的元素数量可以通过指定合适的容量大小让集合初始时就具备足够的空间来存储元素。这样可以减少集合在插入元素时的扩容次数从而提高插入元素的性能。如果预先知道集合可能存储的元素数量直接将容量设置为预期数量可以避免多次扩容。减少内存浪费 如果不指定容量集合会使用默认的初始容量。在集合元素逐渐增加的过程中如果容量不够会触发扩容操作将容量翻倍这可能会造成一定的内存浪费。而如果能够根据预期的元素数量来初始化集合的容量可以避免过多的内存浪费。
而且容量的设置并不会影响集合的逻辑功能只是在性能和内存利用方面有所优化。过小的容量可能导致频繁的扩容过大的容量可能会浪费内存因此建议根据预期的元素数量来合理设置集合的容量。
通过4.8近而衍生出另一个问题
4.9 HashMap 的初始容量设置成多少合适
大家都知道…咳咳这道是送命题为啥因为我送过。这道题如果你回答出一个值任何值都是错误的。
因为初始容量设置成多少合适取决于你对元素数量的预估和性能需求。一般情况下使用你预计的值除以默认的负载因子0.75所得的值作为初始容量最合适。比如我认为这个功能大概会有100个数据那么100/0.75133那么我就会创建
HashMapString, Integer hashMap new HashMap(133);使用133作为Capacity的值。
面试简单的说
使用预计的总数值除以默认的负载因子0.75所得的数值作为初始容量最合适然后根据具体场景和数据量的预估再适当进行调整减少扩容的次数。
4.10 默认负载因⼦0.75
大家都知道默认负载因⼦为0.75有很多人好奇为什么默认负载因⼦会设置为0.75这个跟内存利用率、性能、哈希冲突都有关系
内存利用率0.75的负载因子在元素数量和容器大小的比率上取得了一个较好的平衡使得当HashMap中的元素个数达到容器大小的3/4时就会触发扩容操作。这样可以在一定程度上避免过多的内存浪费。性能较小的负载因子可以减少哈希冲突的概率提高HashMap的性能。但同时过小的负载因子会导致容器过早地进行扩容影响性能。0.75的负载因子在性能和扩容频率之间找到了一个较好的平衡点。哈希冲突较小的负载因子可以减少哈希冲突的发生频率减少链表或红黑树的长度提高查找元素的效率。当哈希冲突较少时链表或红黑树的长度较短查找速度更快。
面试简单的说
背不下的背这个
0.75的负载因子在大多数场景下能够提供较好的性能和空间利用率。
4.11 HashMap 的线程安全问题
大家都知道HashMap是非线程安全的容器尤其是在多线程的情况下对同一个HashMap对象进行插入、删除、修改操作时会导致数据不一致和丢失问题。
对策就是使用ConcurrentHashMap估计差不多是所有人都会背的一个了。
但是其实你也可以用另外一种方式这种面试官也会经常问如果不用ConcurrentHashMap,你该怎么办讲不出来那你就GG了。
第一种
使用Collections.synchronizedMap获取同步map
MapString, Integer synchronizedMap Collections.synchronizedMap(new HashMap());第二种
使用synchronized
MapString, Integer hashMap new HashMap();
synchronized (hashMap) {// 对hashMap进行操作
}当然了装逼归装逼为了保证多线程下的hashmap的数据一致性我还是会乖乖使用ConcurrentHashMap并且ConcurrentHashMap的效率也比较高。别问我为什么因为只要你有过生产环境出问题客户在现场参观甲方一直催的经历的话你也会乖乖使用ConcurrentHashMap。
4.12 HashMap 转红黑树的阈值设置为 8
大家应该都遇过面试关于HashMap的时候总会问你转红黑树的阈值问题。
在JDK 8中Java对HashMap进行了一些优化其中一个改进是引入了红黑树来优化链表过长的问题。当链表长度为8时HashMap会将链表转成红黑树以便提高查找和插入的性能。
别问为什么这是开发者经过实验和性能测试得出的一个经验值链表长度达到8的概率较低此时转换为红黑树可以显著减少查找元素的时间复杂度从而提高HashMap的性能。
链表长度较小时使用链表进行查找是较为高效的。因为链表进行插入和查找的时间复杂度都是O(1)但是一旦链表过长查找时间会变成O(n)n为长度。而红黑树查找的时间复杂度为O(log n)明显时间较短性能更高。但是如果使用红黑树存储较少数据的时候红黑树额外的开销会影响性能。红黑树相对于链表来说存储每个节点需要更多的内存空间因为红黑树需要维护额外的颜色信息、左右子节点指针等。在元素较少的情况下使用红黑树可能会占用更多的内存。且插入和删除还需要进行自平衡调整这些都是额外开销。因此将链表转换为红黑树是在平衡链表长度和查找性能之间进行的权衡而8作为阈值在大多数场景中表现较好。
总结一下在JDK 8中HashMap将链表转换为红黑树的阈值设置为8的原因
性能优化JDK 8对HashMap进行了优化引入了红黑树来解决链表过长导致查找性能下降的问题。阈值设置当某个桶中的链表长度达到8时HashMap会将链表转换为红黑树以提高查找和插入的性能。经验值将阈值设置为8是经过实验和性能测试得出的经验值。在典型的应用场景中链表长度达到8的概率较低此时转换为红黑树可以显著减少查找时间。权衡将链表转换为红黑树是在链表长度和查找性能之间进行的权衡。较短的链表使用链表进行查找是高效的较长的链表使用红黑树可以提高性能。
总体来说将链表转换为红黑树的阈值设置为8在大多数场景中表现较好能够在提高查找性能的同时不引入过多的额外开销。不过具体的阈值设置也可以根据实际应用场景进行调整。HashMap在JDK 8中的这一改进使其在大规模数据和高并发场景下的性能得到显著提升。