古法编程java之集合、List

皮卡丘不是竹鼠 2026-06-17 18:39 1

集合


引入集合


对于入门程序来说,保存数据通常用的是数组,但数组有三个不足的地方:




  1. 数组的长度一开始就指定了




  2. 数组往往存的都是相同类型的元素




  3. 数组想要扩容是十分困难的




那集合有什么好处呢?




  1. 它可以动态地保存任意多个对象,使用起来很方便




  2. 它提供了一系列方便操作对象的方法,比如有 add、remove、set、get 等等




  3. 使用集合添加、删除元素十分简洁




单列集合:每次存一个数据


Iterable(接口)
└── Collection(接口)
├── List(接口)
│ ├── Vector(类)
│ ├── ArrayList(类)
│ └── LinkedList(类)
└── Set(接口)
├── TreeSet(类)
└── HashSet(类)

双列集合:每次存一个键值对


Map(接口)
├── Hashtable(类)
│ └── Properties(类)
├── HashMap(类)
│ └── LinkedHashMap(类)
└── TreeMap(类)

Collection接口常用方法


Collection 接口实现类的特点:




  1. Collection 实现子类可以存放多个元素,每个元素都可以是 Object 类型




  2. 有些 Collection 的实现类可以存放重复的元素,有些不可以




  3. 有些 Collection 的实现类是有序的,有些是无序的(比如 List 是有序的,Set 是无序的)




  4. Collection 接口没有直接的实现子类,是通过它的子接口 Set 和 List 来实现的




因为 Collection 接口本身不能被实例化,所以常用方法以下全用 ArrayList 来展示。


// add方法:添加元素
ArrayList arrayList = new ArrayList();
arrayList.add("hello");
arrayList.add(10);
arrayList.add(true);
System.out.println(arrayList);
// remove 方法:删除指定位置的元素(或指定元素)
arrayList.remove(1);
arrayList.remove("hello");
System.out.println(arrayList);
// contains 方法:判断集合中是否包含某个元素
System.out.println(arrayList.contains(true));
// size 方法:获取集合中的元素个数
System.out.println(arrayList.size());
// isEmpty 方法:判断集合是否为空
System.out.println(arrayList.isEmpty());
// clear 方法:清空整个集合。
arrayList.clear();
System.out.println(arrayList);
// addAll 方法:把另一个集合里的数据全放进去
ArrayList arrayList1 = new ArrayList();
arrayList1.add("红楼梦");
arrayList1.add(false);
arrayList.addAll(arrayList1);
System.out.println(arrayList);
// containsAll 方法:查找是否多个元素都存在
System.out.println(arrayList.containsAll(arrayList1));
// removeAll 方法:删除多个元素
arrayList.add("西游记");
arrayList.removeAll(arrayList1);
System.out.println(arrayList);

迭代器遍历


基本介绍


Iterator 对象称为迭代器,主要有以下三点:




  1. 主要用于遍历 Collection 集合中的元素。




  2. 所有实现了 Collection 接口的集合类都有一个 iterator() 方法,用来返回一个实现了 Iterator 接口的对象,即可以返回一个迭代器。




  3. Iterator 仅用于遍历集合,它本身并不存放对象。




基本使用方法


迭代器主要有两个方法:一个是 next,一个是 hasNext。




  1. next 方法:用于将迭代器的游标指向下一个位置




  2. hasNext 方法:负责判断是否还有下一个位置的元素




因此,在每次使用 next 方法前,都要用 hasNext 去进行一次判断。


public static void main(String[] args) {
Collection col = new ArrayList();

col.add(new Book("java", "james", 100.0));
col.add(new Book("三国演义", "罗贯中", 150.0));
col.add(new Book("水浒传", "施耐庵", 120.0));

// 现在希望能够遍历集合
// 先得到集合对应的迭代器
Iterator it = col.iterator();
// 使用while循环遍历即可,遍历前要先用 hasNext 进行检测,然后再用 next 进行游标的移动
while (it.hasNext()) {
Object obj = it.next();
System.out.println(obj);
}
// 使用itit,可以快捷生成遍历集合
while (it.hasNext()) {
Object next = it.next();

}

// 当迭代器退出 while 循环后,iterator迭代器指向最后的元素,不能再用 next 方法找下一个元素
// 否则会报 NoSuchElement异常。
// 解决办法:重新获取一个迭代器即可
it = col.iterator();
}

增强for循环


可以替代 iterator 迭代器,增强 for 其实底层用的也是 Iterator 迭代器,不信的话可以打断点自己进去看


Collection col = new ArrayList();

col.add(new Book("java", "james", 100.0));
col.add(new Book("三国演义", "罗贯中", 150.0));
col.add(new Book("水浒传", "施耐庵", 120.0));

// 直接使用增强 for 循环也可以遍历
for (Object book : col) {
System.out.println(book);
}

快捷键上可以用 col.iterator() 或者 col.iter 都可以


小练习




  1. 创建 3 个 Dog {name, age} 对象,放入到 ArrayList 中,赋给 List 引用




  2. 用迭代器和增强 for 循环两种方式来遍历




  3. 重写 Dog 的 toString 方法,输出 name 和 age




public static void main(String[] args) {
List list = new ArrayList();
list.add(new Dog("旺财",10));
list.add(new Dog("小花",5));
list.add(new Dog("小黄",3));
Iterator iterator = list.iterator();
while (iterator.hasNext()) {
Object next = iterator.next();
System.out.println( next);
}

System.out.println("===================");
for (Object dog : list) {
System.out.println(dog);
}
}

没什么难度,一道小练手题,就不讲了。


List接口


基本特性介绍


List 接口中的元素具有以下特性:



  1. 元素是有序的,即添加顺序和取出顺序一致,且元素可重复。

  2. 集合中的每个元素都有其对应的索引,即支持索引。

  3. 容器中的元素都对应一个整数型的序号,记载它在容器中的位置,可以根据序号取出元素。

  4. List 接口常用的实现类有 ArrayList、LinkedList 和 Vector。


基本方法介绍


List 集合里添加了一些根据索引来操作集合元素的方法



  1. void add(int index, Object ele):在index位置插入ele元素

  2. boolean addAll(int index, Collection eles):从index位置开始将eles中的所有元素添加进来

  3. Object get(int index):获取指定index位置的元素

  4. int indexOf(Object obj):返回obj在集合中首次出现的位置

  5. int lastIndexOf(Object obj):返回obj在当前集合中末次出现的位置

  6. Object remove(int index):移除指定index位置的元素,并返回此元素

  7. Object set(int index, Object ele):设置指定index位置的元素为ele,相当于是替换.

  8. List subList(int fromIndex, int toIndex):返回从fromIndex到toIndex位置的子集合


小练习


添加10个以上的元素(比如String “hello” ),在2号位插入一个元素"hspedu",获得第5个元素,删除第6个元素,修改第7个元素,在使用迭代器遍历集合,要求:使用List的实现类ArrayList完成。


public static void main(String[] args) {
ArrayList arrayList = new ArrayList();
arrayList.add("jack");
arrayList.add("tom");
arrayList.add("mary");
arrayList.add("bob");
arrayList.add("smith");
arrayList.add("jsp");
arrayList.add("kokin");
arrayList.add("xiaohu");
arrayList.add("knight");
arrayList.add("keria");

arrayList.add(1, "hspedu");;
arrayList.get(4);
arrayList.remove(5);
arrayList.set(6, "hspedu");

for (Object o : arrayList) {
System.out.println(o);
}
}

没什么难度,就不详细说了。


ArrayList


ArrayList注意事项


注意事项:



  1. ArrayList 允许所有元素,并且可以存入 null,而且可以存入多个 null

  2. ArrayList 是由数组来实现数据存储的

  3. ArrayList 基本等同于 Vector,除了 ArrayList 是线程不安全的,但它的执行效率更高。因此,在多线程情况下,不建议使用 Array


ArrayList扩容机制


底层操作机制源码分析:




  1. ArrayList 中维护了一个 Object 类型的数组 elementData。




  2. 每次创建 ArrayList 对象时,如果使用的是无参构造器,则初始化 elementData 容量为 0。第一次添加时扩容 elementData 为 10;如果需要再次扩容,则扩容为原来的 1.5 倍。




  3. 如果使用的是指定大小的构造器,则初始 elementData 容量为指定大小;如果需要扩容,则直接扩容 elementData 为 1.5 倍




  4. 如果一次要新增加的容量太大,超过了 1.5 倍,那就按要增加的容量算。




阅读源码


先来看 ArrayList 的无参构造器,很明显就是赋给 elementData 一个空数组。
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

再来看带初始化容量的构造器
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity]; // 如果初始容量大于 0,那就直接 new 一个相应容量的 Object 数组
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA; // 如果等于 0,就赋给它一个常量空数组。这和之前那个空参构造的数组还不一样
} else {
throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity);
}
}

先来,再来看一看 add 方法。
public boolean add(E e) {
modCount++; //记录集合结构修改的次数
add(e, elementData, size);// 调用下方的 add 方法
return true; // 返回修改结果
}

private void add(E e, Object[] elementData, int s) {
if (s == elementData.length) // 检查容量是否足够
elementData = grow(); //容量不够就考虑扩容
elementData[s] = e; // 添加元素
size = s + 1; // 更新大小
}

private Object[] grow() {
return grow(size + 1);
}

private Object[] grow(int minCapacity) {
int oldCapacity = elementData.length;
if (oldCapacity > 0 || elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { // 如果不是空数组
int newCapacity = ArraysSupport.newLength(
// 旧容量
oldCapacity,
// 最小增长量
minCapacity - oldCapacity,
// 首选增长量,右移一位就相当于除以 2,所以这里的首选增长量其实就是指增长 0.5 倍,变成原来的 1.5 倍
oldCapacity >> 1);
// newLength 方法会在首选容量和最小需求容量中取较大值。也就是说,它会尽可能地按照 1.5 倍原容量的方式去增长,除非你一次性要添加的容量大于它。
return elementData = Arrays.copyOf(elementData, newCapacity);
} else { // 首次添加元素(空数组)
// private static final int DEFAULT_CAPACITY = 10,minCapacity通常为1,因此这里就是扩容为10
return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)];
}
}

Vector


首先,Vector 的底层也是一个对象数组,即 Object elementData。


第二点,Vector 是线程同步的,所以开发中如果需要考虑线程安全,可以考虑使用 Vector。


Vector的扩容机制


如果是无参,默认初始化有 10 的容量,满了之后按两倍扩容。如果是指定大小,那也每次按两倍扩容。


来看看源码


public Vector() {
this(10); //可以看出,用无参构造的话,默认就是 10
}
public Vector(int initialCapacity) {
this(initialCapacity, 0);
}
public Vector(int initialCapacity, int capacityIncrement) {
super();
// 先做一个异常校验,如果初始容量是负数的话,抛出异常
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity);
this.elementData = new Object[initialCapacity]; // 创建用来存储数据的数组
this.capacityIncrement = capacityIncrement; // 为 0 表示使用默认的翻倍扩容策略
}

// add 方法和 ArrayList 的类似,就不额外分析了。但注意,这是线程安全的。
public synchronized boolean add(E e) {
modCount++;
add(e, elementData, elementCount);
return true;
}

private void add(E e, Object[] elementData, int s) {
if (s == elementData.length)
elementData = grow();
elementData[s] = e;
elementCount = s + 1;
}

private Object[] grow() {
return grow(elementCount + 1);
}

private Object[] grow(int minCapacity) {
int oldCapacity = elementData.length;
int newCapacity = ArraysSupport.newLength(
oldCapacity, // 旧容量
minCapacity - oldCapacity, // 最小增长容量
// 这里的意思就是,如果这个增长系数大于 0(也就是说不是零的时候),它就直接按照你设定的容量去增长多少容量。如果是零的话,就直接按旧容量的数量去增长。既然增长的数量和旧容量一样,那不就是翻倍增长吗?
capacityIncrement > 0 ? capacityIncrement : oldCapacity);
return elementData = Arrays.copyOf(elementData, newCapacity);
}

LinkedList


LinkedList 底层:



  1. LinkedList 底层维护了一个双向链表。

  2. LinkedList 的底层维护了两个属性,分别是 first 和 last,它们分别指向首节点和尾节点。

  3. 每个节点是一个 Node 对象,里面又维护了 prev、next 和 item 三个属性。其中通过 prev 指向前一个节点,通过 next 指向后一个节点,最终实现双向链表。

  4. 因此 LinkedList 的元素的添加和删除不是通过数组完成,相对来说效率较高


有两点需要注意:



  1. LinkedList 可以添加任意元素,包括 null,并且元素可以重复

  2. 它线程也是不安全的,没有实现线程同步


LinkedList源码阅读


public LinkedList() {
// 如果初始化的时候是空参,那就什么也不做。
}

public boolean add(E e) {
linkLast(e); // 把元素链接到链表尾
return true;
}

void linkLast(E e) {
final Node<E> l = last; // 取出当前的链表尾节点
final Node<E> newNode = new Node<>(l, e, null); // 创建一个新节点
last = newNode; // 新节点就是最后一个节点了
if (l == null) // 如果原来的尾节点是空,就说明原来的链表是空的
first = newNode; // 那新节点同时也是整个链表的头节点
else
l.next = newNode; // 不然的话,就让原来的尾节点的下一个引用也指向新节点
size++;
modCount++;
}
// 初始化一个节点对象
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}

几种List子类的选择方法


ArrayList 的底层是可变数组,因此它改查的效率比较高;LinkedList 的底层是双向链表,因此它增删的效率比较高。


如果改查的操作多,就选择 ArrayList;如果增删的操作多,则选择 LinkedList。


问了一下 AI,基本上绝大多数情况就是 ArrayList,另外两种 Vector 和 LinkedList 使用情况极少。

最新回复 (12)
  • XTer 06-17 18:40
    1

    好、好怀念


    这就是古法编程吗


    好久没自己写过一行真正的代码了

  • XiaoQ1 06-17 18:44
    2

    八股文还得背啊,面试会考啊,好难啊我去

  • 小象超市 06-17 18:44
    3

    有点泪目了,突然猛然想起自己刚实习、刚工作的时候为了上线可以通宵手搓代码。


    突然想起来自己也是会写代码的人

  • 想不到叫啥名 06-17 18:49
    4

    ^-^ 这东西我只看,碰都不碰的。

  • Ken 06-17 18:52
    5

    vector基本是上一个时代的东西了,需要线程同步一般用CopyOnWriteArrayList




    总觉得你在水贴但是我没有证据

  • GeeChange 06-17 18:53
    6

    还看的下去吗现在, ^-^已经没有看的欲望了

  • stefan啦啦啦 06-17 18:56
    7

    这是什么,看不懂了已经, 传说中的java嘛

  • fablia 06-17 18:56
    8

    避免人类文明断代

    现开始选拔古法编程传承人

  • 袭明 06-17 18:58
    9

    教教我,黑马老师.jpg


    我感觉你要是把黑马教程全发一遍,发到第三四篇的时候就要被当水贴了……

  • Mikami Yua 06-17 19:13
    10

    你把论坛当成你的知识库记笔记呢?

  • 诺 陈 06-17 19:14
    11

    我还以为你让 jvaver 集合呢,还有就是记的太乱了

  • Mumu 06-17 19:17
    12

    可以提名为L站非遗文化传承项目。

* 帖子来源Linux.do
返回