包装类
包装类(Wrapper Class)在编程中,特别是Java等面向对象的编程语言中,扮演着重要的角色。以下是对包装类的详细解释:
一、定义
包装类是针对基本数据类型定义的引用类型。在Java中,有八种基本数据类型(byte, short, int, long, float, double, char, boolean)和它们对应的包装类(Byte, Short, Integer, Long, Float, Double, Character, Boolean)。此外,Java还提供了BigInteger
和BigDecimal
类来处理超过基本数据类型范围的大数。
二、作用
- 方便操作基本数据类型:包装类提供了丰富的方法来操作对应的基本数据类型,如类型转换、字符串转换等。
- 集合操作:Java集合类(如List, Set等)只能存放引用类型的数据,不能存放基本数据类型。通过包装类,可以将基本数据类型转换为对象类型,从而放入集合中。
- 自动装箱与拆箱:从Java 1.5(JDK 5.0)开始,引入了自动装箱(Autoboxing)和拆箱(Unboxing)机制,使得基本数据类型和包装类之间的转换更加简便。自动装箱是指将基本数据类型自动转换为包装类对象,而拆箱则是指将包装类对象转换回基本数据类型。
- 数据安全性:包装类作为对象,可以通过null值来表示空,这在某些情况下比基本数据类型的默认值(如0, false等)更具表现力。
三、基本类型与包装类型的区别
- 存储位置:基本数据类型直接存储在堆栈中,而包装类对象则通过引用指向实例,实例保存在堆中。
- 初始化:基本数据类型有默认的初始值(如int默认为0),而包装类对象的初始值为null,需要显式创建实例并赋值。
- 功能:包装类提供了比基本数据类型更多的功能,如类型转换、字符串转换等。
四、示例
|---|----------------------------------------------------------------|
| | // 自动装箱
|
| | Integer num = 10; // 相当于 Integer num = Integer.valueOf(10);
|
| | |
| | // 拆箱
|
| | int i = num.intValue(); // 或者直接 int i = num; 在Java 5及以上版本
|
| | |
| | // 集合中使用包装类
|
| | List<Integer> list = new ArrayList<>();
|
| | list.add(1); // 自动装箱
|
| | int second = list.get(0); // 自动拆箱
|
| | |
| | // 字符串转换
|
| | String str = Integer.toString(num);
|
| | int numFromStr = Integer.parseInt(str);
|
五、注意事项
- 性能考虑:在性能敏感的场景下,需要注意自动装箱和拆箱可能带来的性能开销。
- 空指针异常:由于包装类对象可能为null,因此在使用包装类对象时需要特别注意空指针异常的问题。
- 缓存机制:Java对Integer等包装类实现了缓存机制,对于-128到127之间的值,会缓存其对应的包装类对象。这意味着在这个范围内的值进行装箱时,可能会返回同一个对象。但这并不意味着所有包装类都有这样的缓存机制。
综上所述,包装类在Java等面向对象的编程语言中扮演着重要的角色,它们不仅提供了对基本数据类型的封装和扩展功能,还使得基本数据类型能够更方便地参与到面向对象的编程中。
ArrayList
1.ArrayList类继承体系
ArrayList类的继承体系在Java中是非常明确的。ArrayList是Java集合框架中的一个重要类,它主要继承自AbstractList<E>
类,并实现了List<E>
接口、RandomAccess
接口、Cloneable
接口和java.io.Serializable
接口。
这一继承体系使得ArrayList具备了丰富的功能和特性,包括动态数组的实现 、随机访问 、克隆能力 以及序列化能力等。
具体来说,ArrayList的继承体系可以表示为:
|---|----------------------------------------|
| | java.lang.Object
|
| | └── java.util.AbstractCollection<E>
|
| | └── java.util.AbstractList<E>
|
| | └── java.util.ArrayList<E>
|
ArrayList实现接口
List<E>
:提供了列表操作的基础接口,如添加、删除、遍历等。RandomAccess
:表明ArrayList支持随机访问,即可以通过索引快速访问列表中的元素。Cloneable
:表明ArrayList可以被克隆。java.io.Serializable
:表明ArrayList支持序列化,可以将对象状态保存到文件中或通过网络传输。
add底层代码解析
public void add(int index, E element) {
// 首先,检查索引是否有效,即是否在0到size之间(不包括size)
rangeCheckForAdd(index);
// 每当结构被修改时,modCount增加
modCount++;
final int s; // 临时变量,用于存储当前列表的大小
Object[] elementData; // 临时变量,用于引用elementData数组
// 如果当前列表大小等于其底层数组的大小,则需要进行扩容
if ((s = size) == (elementData = this.elementData).length)
elementData = grow(); // 调用grow方法进行扩容
// 将从index位置开始的元素向后移动一位,为新元素腾出空间
System.arraycopy(elementData, index,
elementData, index + 1,
s - index);
// 在指定位置插入新元素
elementData[index] = element;
// 更新列表的大小
size = s + 1;
}
关键步骤解析
索引有效性检查:
通过rangeCheckForAdd(index)方法检查传入的索引值index是否有效。如果索引小于0或大于等于当前列表的大小size,则会抛出IndexOutOfBoundsException。
扩容处理:
如果当前列表的大小(size)等于底层数组elementData的长度,说明数组已经满了,无法再添加更多元素。此时,会调用grow()方法进行扩容。grow()方法通常会将数组的大小增加到原来的1.5倍(具体实现可能因JVM版本而异),然后返回新的数组。注意,这里的elementData变量在扩容后会被更新为指向新的数组。
元素移动:
使用System.arraycopy方法将索引index及之后的元素向后移动一个位置,为新元素腾出空间。System.arraycopy是一个本地方法,它提供了比手动循环复制更高的性能。
插入新元素:
在腾出的位置(即索引index处)插入新元素element。
更新列表大小:
将列表的大小size增加1,以反映新添加的元素。
sout底层代码
在Java中,当你使用System.out.println()
来打印一个对象时(包括集合对象如ArrayList
),输出之所以不是内存地址(即对象的引用在内存中的直接表示),是因为Java的Object
类(所有类的超类)提供了一个toString()
方法,该方法被设计为返回对象的字符串表示。对于ArrayList
和其他许多Java集合类,toString()
方法已经被重写以提供更有用的信息,即集合中元素的字符串表示。
return (obj == null) ? "null" : obj.toString();
对象自动调用tostring将object转为String型;
forEach
方法
是 JavaScript 中一个常用于数组(Array)的方法,它允许你对数组的每个元素执行一个回调函数。这个回调函数会接收三个参数:当前元素的值、当前元素的索引(可选)、以及正在被遍历的数组本身(可选)。但是,需要注意的是,forEach
方法不会改变原数组,也不会返回新的数组,它只是用来遍历数组中的每个元素。
如果在遍历过程中修改了集合(modCount变化),则抛出ConcurrentModificationException
LinkedList
1.add(E e)
:
在列表末尾添加指定的元素。
add(int index, E element)
: 在列表的指定位置插入指定的元素。
2.get(int index)
:
返回列表中指定位置的元素。
3.迭代器和for each的区别
for each
循环(也称为增强型 for
循环)和迭代器(Iterator)在Java中都是用于遍历集合(如List、Set)和数组的重要工具,但它们之间存在一些关键的区别。以下是两者的主要区别:
1. 底层实现
- for each 循环 :
for each
循环是Java 5(JDK 1.5)引入的一种语法糖,用于简化集合和数组的遍历。其底层实际上是通过迭代器(Iterator)来实现的。因此,当你使用for each
循环遍历集合时,Java编译器会自动为你处理迭代器的创建和遍历逻辑。 - 迭代器(Iterator) :迭代器是一个专门用于遍历集合的接口,它提供了统一的方法来遍历集合中的元素,而不需要了解集合的内部结构。迭代器接口包含
hasNext()
,next()
,remove()
等方法,用于检查序列中是否还有更多元素、获取序列中的下一个元素以及从迭代器遍历的集合中移除迭代器最后返回的元素。
2. 使用方式
-
for each 循环 :使用简单直观,代码更简洁。它隐藏了迭代器的创建和遍历细节,使得遍历集合或数组的操作更加容易理解。
|---|---------------------------------------------|
| |for (ElementType element : collection) {
|
| |// 处理元素
|
| |}
| -
迭代器 :使用迭代器需要显式地获取迭代器的实例,并调用其方法来遍历集合。这提供了更多的灵活性,但代码相对复杂一些。
|---|------------------------------------------------------------|
| |Iterator<ElementType> iterator = collection.iterator();
|
| |while (iterator.hasNext()) {
|
| |ElementType element = iterator.next();
|
| |// 处理元素
|
| |}
|
3. 功能差异
- for each 循环 :虽然简单易用,但在遍历过程中不能直接修改集合(如添加或删除元素),除非集合本身支持并发修改(如
CopyOnWriteArrayList
),否则可能会抛出ConcurrentModificationException
异常。这是因为for each
循环底层使用迭代器,而迭代器在遍历过程中默认不允许修改集合。 - 迭代器 :迭代器提供了
remove()
方法,允许在遍历过程中安全地删除元素(即迭代器最后返回的元素)。但是,尝试在调用next()
方法之前或调用next()
方法之后调用remove()
方法将抛出IllegalStateException
异常。
4. 性能考虑
- 在大多数情况下,
for each
循环和迭代器的性能差异可以忽略不计,因为它们底层都使用了类似的机制来遍历集合。然而,在某些特定情况下(如遍历LinkedList
时),迭代器的性能可能会略优于for each
循环,因为LinkedList
的随机访问性能较差,而迭代器可以通过顺序访问来优化性能。
总结
for each
循环和迭代器都是Java中遍历集合和数组的有效工具。for each
循环以其简洁易用的特点受到广泛欢迎,而迭代器则提供了更多的灵活性和控制力。在选择使用哪种方式时,应根据具体的需求和场景来决定。
4.一些信息
LinkedList
在 Java 中是一个基于链表的数据结构,它实现了 List
接口和 Deque
接口。LinkedList
可以包含任何类型的对象,因为它是泛型的。这意味着你可以为 LinkedList
指定一个具体的类型参数,使其只能存储该类型的对象或该类型的子类型的对象。
LinkedList
的数据类型(即它可以包含的元素类型)可以是任何类(包括自定义类)的实例,或者是 Java 提供的任何基本数据类型的包装类(如 Integer
、Double
、Character
等,对应于基本类型 int
、double
、char
等)。