Java 中 List.subList() 方法的使用陷阱

article/2025/10/21 20:11:10

转载请注明本文出自 clevergump 的博客:http://blog.csdn.net/clevergump/article/details/51105235, 谢谢!


前言

本文原先发表在我的 iteye博客: http://clevergump.iteye.com/admin/blogs/2211979, 但由于在 iteye发表的这篇文章的某些渲染曾经出现过一些问题, 我曾发过多封邮件向 iteye 的工作人员进行反馈, 官方只是在我第一封邮件中回复说会联系技术人员处理, 但是此后就再也没有收到他们的任何回复了, 我后来多次邮件询问进度, 也没有收到新的回复, 及时响应用户的提问和需求, 是提高用户体验和满意度的重要因素. 我想, 即使你们不能搞定或者不想去处理, 也应该给个回复, 即使回复 “我们暂时无法处理”, 也比不回复邮件要好啊. 于是我就决定今后永远放弃 iteye, 但自认为原博客中的这篇文章还是有一定价值的, 于是就在此重新发表一次, 并且今后就维护这篇文章吧.

另外多说一句, 不论你是从事什么行业的什么工作, 只要是和响应客户需求相关的岗位, 都应该主动及时地让客户知道他们所关心的事情的处理进度, 这样才能提高用户体验和满意度. 包括我们做手机 APP 或者 PC 客户端开发的岗位也是如此, 我们的客户端在遇到网络异常, 或者内存不足, 或者一段时间内无响应等特殊情况, 都应该及时主动地给用户弹出一个提示框或对话框. 我们在下载软件时, 显示下载进度条, 在加载图片时, 显示加载进度条. 在用户点击任何一个可能会让他们认为 (包括误认为) 可以点击的地方, 都必须要么进行页面变化, 要么弹出一个对话框或提示框, 不能什么都不处理, 尤其是对于那些设计时没有添加点击功能, 但却有可能被用户误认为可以点击的地方, 也要做相应的用户点击的响应处理……以上这些做法, 都是为了让用户及时知道他们关心的事情的进度, 都是提升用户体验的做法. 好了, 前言就扯这么多吧.


正文

做 Java 或 Android 开发的朋友, 一定都很熟悉 String 类中的 subString() 方法. 下面我们先来看一个关于该方法的小例子. 假如我们有如下需求: 随意设定一个字符串, 然后从中取出一个子字符串, 然后在该子字符串的末尾添加一些新的字符, 但要保证原先的字符串不变. 这个需求对你来说实在是 so easy, 于是你迅速写出了如下代码:

public class SubStringDemo {private static String str;private static String subStr;public static void main(String[] args) {subStringTest();}private static void subStringTest() {str = "01234";subStr = str.substring(2, str.length());print();subStr += "5";System.out.println("---------此时将 subStr 中增加一个字符 '5' ----------");print();}private static void print() {System.out.println("str    = " + str);System.out.println("subStr = " + subStr);}
}

你假设原字符串为 “01234”, 通过 subString() 方法从该字符串中取出一个子字符串 “234”, 然后在这个取出的子字符串的末尾添加一个新的字符’5’, 这样子字符串就变为 “2345”, 而原字符串则不变, 仍为 “01234”.

我们看下运行结果:

这里写图片描述

从运行结果来看, 代码确实没问题. IQ极高的你甚至有些愤愤不平, “这么 low 的需求, 简直就是在欺 (wu) 负 (ru) 哥的智商嘛”, 不知情的人, 还以为你在卫生间看到了下面这张图呢:

这里写图片描述

哈哈, 你可能确实有点屈才了. ^_^

没关系, 既然你智商很高, 我们就改个需求吧, 要求你能快速响应我们的需求变化, 要体现在代码中. 你说, 没问题, 尽管放马过来吧, 哥都能 hold 住. 于是需求改为如下内容: 将原需求中的字符串改为 List (也就是 java.util.List ), 将原需求中所有对字符串的要求都移植到对 List 的要求中. 具体来说就是, 随意设定一个 List 的实现类对象, 然后从中取出一个子 List , 然后向该子 List 中添加一些新的元素, 但要保证原先的 List 不变.

看到这个需求后, 估计你的心情可能又会像上面那张图那样吧. 这个变化, so easy. StringsubString() 方法, 难道 List 就没有 subList() 方法??? 人要学会融会贯通嘛, 所以答案是显而易见的. 如果这都不是欺 (wu) 负 (ru) 哥的智商的话, 那么世界上就不存在 “欺 (wu) 负 (ru) 智商” 的说法了. 但是, 你终究还是平复了你的心情, 然后奋笔疾书, 快速写下了如下代码:

private static List list;
private static List subList;private static void subListTest(Class<? extends List> listClazz) throws IllegalAccessException, InstantiationException {if (listClazz == null) {throw new IllegalArgumentException(listClazz + " is null.");}list = listClazz.newInstance();list.clear();for (int i = 0; i < 5; i++) {list.add(i);}subList = list.subList(2, list.size());subList.add(5);
}

和先前 String 需求中设定的数字类似, 你在原 List 中设定该 List 中存有5个元素, 分别是整数 0, 1, 2, 3, 4. 然后将第2个元素到最末一个元素全部取出, 作为子 List. 然后向取出的这个子 List 中添加一个整数5. 写完这个代码后, 你甚至根本没有进行自测, 就非常自信地把代码直接交给了测试MM.

然而, 过了一会儿, 测试MM反馈说, 你的代码有bug. 在子 List 新增元素后, 原 List 也变了. 你很诧异, 不可能呀, 不应该呀, 子 List 的变化, 怎么会影响到原 List 呢? 不可能的, 一定是测试MM搞错了, 你心里或许在想, 难道是因为哥长得帅, 妹子想借此搭讪哥? ^_^ 但是, 测试MM一脸正经地告诉你, 确实有bug, 你的确需要修复, 先提个 bug 跟进的单子吧. 此刻, 你感觉到情况似乎有些不妙, 为了谨慎起见, 你立刻对原先的代码进行自测, 在原先代码的基础上增加了一些日志输出语句, 于是就有了如下代码:

public class SubListDemo {private static List list;private static List subList;public static void main(String[] args) {try {System.out.println("/*--------------------------- ArrayList -----------------------------------*/");subListTest(ArrayList.class);System.out.println("");System.out.println("/*--------------------------- LinkedList -----------------------------------*/");subListTest(LinkedList.class);} catch (Exception e) {e.printStackTrace();}}private static void subListTest(Class<? extends List> listClazz) throws IllegalAccessException, InstantiationException {if (listClazz == null) {throw new IllegalArgumentException(listClazz + " is null.");}list = listClazz.newInstance();list.clear();for (int i = 0; i < 5; i++) {list.add(i);}subList = list.subList(2, list.size());print();subList.add(5);System.out.println("---------此时将子list中增加一个元素 5 ----------");print();}private static void print() {System.out.println("原 list: " + list);System.out.println("子 list: " + subList);}
}

你对 List 接口最常用的两个实现类 ArrayListLinkedList 都分别做了测试, 得到如下的打印结果:

这里写图片描述

在子 List 增加了元素5以后, 原先的 List 也相应增加了元素 5, 留意上图中的两个蓝色圆圈.
于是你又将增加的元素改为另外一个数字, 比如: 10, 你会发现, 原 List 也会增加元素 10.
而如果你将增加元素改为删除元素, 例如: 删除坐标为0的元素, 即: 将 subListTest() 方法改为如下代码:

private static void subListTest(Class<? extends List> listClazz) throws IllegalAccessException, InstantiationException {if (listClazz == null) {throw new IllegalArgumentException(listClazz + " is null.");}list = listClazz.newInstance();list.clear();for (int i = 0; i < 5; i++) {list.add(i);}subList = list.subList(2, list.size());print();subList.remove(0);System.out.println("---------此时将子list中的第0个元素删除 ----------");print();
}

打印结果如下:

这里写图片描述

你会发现, 当你删除子 List 中的第0个元素, 也就是元素2的时候, 原先的 List 中的元素2也被一同删除了, 还是留意上图中的蓝色圆圈标注的数字, 这是原 List 中的元素2, 他们在子 List 执行删除动作以后, 也会被一同删除掉.

奇怪呀, 为什么向子 List 中增加或删除一个元素, 会同时让原 List 也增加或删除相同的元素呢? 此刻的你陷入了深深的疑惑与不解中…

这里写图片描述

要想解答这个疑惑, 唯有分析源码才是正确的方式啊. 那么, 我们就来分析一下相关的源码吧.

我们就以 ArrayList 为例来进行分析吧. 下面是 ArrayListsubList() 方法的源码:

public List<E> subList(int fromIndex, int toIndex) {  subListRangeCheck(fromIndex, toIndex, size);  return new SubList(this, 0, fromIndex, toIndex);  
}  

该方法其实返回的是 ArrayList 的内部类 SubList 的一个实例, 同时也将当前 ArrayList 对象作为传入该构造方法, 作为第一个参数的值. 我们看看这个构造方法的源码:

private class SubList extends AbstractList<E> implements RandomAccess {private final AbstractList<E> parent;SubList(AbstractList<E> parent,int offset, int fromIndex, int toIndex) {this.parent = parent;this.parentOffset = fromIndex;this.offset = offset + fromIndex;this.size = toIndex - fromIndex;this.modCount = ArrayList.this.modCount;}
}

由上述代码可知, 在创建这个内部类 ArrayList.SubList 的实例时, 会将外部类 ArrayList 的引用作为该内部类对象中 parent 字段的值, 也就是说, 这个 ArrayList.SubList 内部类实例中的 parent 字段会持有外部类 ArrayList 对象的一个引用, 只是添加了一定的偏移量而已. 由于 List 中存放的元素都是引用类型, 而非基本类型, 所以, 这个子 List 中的每一个元素所代表的引用, 其实就和原 List 中在相同索引处偏移 fromIndex 位置后的那个位置上的元素所代表的引用, 二者指向的是相同的对象. 我们换用更直白的方式来说, 就是:
假设有 0~4 这5个整数, 先被分别装箱成5个 Integer对象, 然后被依次添加到原 List 中, 假设我们将原 List 称作 listA, 这时, 这5个对象中的每一个都分别被一个引用指向着, 这些引用刚好就是 listA 中存放的所有元素, 注意: listA中存放的元素其实是引用, 而不是对象本身. 这时, 对 listA 执行了 subList(2, listA.size()) 方法, 创建了一个子 List , 我们将这个子 List 称作 listB. 那么这时, 对象 Integer.valueOf(0) 和 Integer.valueOf(1) 各自还是只被一个引用指向着, 但是, 对象 Integer.valueOf(2), Integer.valueOf(3), Integer.valueOf(4) 却都分别被两个引用指向着, 一个引用来自 listA, 另一个引用来自 listB. 可能上述描述还是不够清晰, 我们用表格来解释吧.

在创建子 List ( 即: listB) 之前, 各个 Integer 对象被引用指向的情况如下:
这里写图片描述

在创建子 List ( 即: listB) 之后, 各个 Integer 对象被引用指向的情况如下:
这里写图片描述
留意红色的字. 在创建了 listB, 也就是子 List 以后, 后三行的三个对象, 都分别被 listA 和 listB中各有一个引用所指向着. 而且还有个规律: listB 中每一个元素(其实这里的元素是引用, 不是对象本身) 所指向的对象, 都会同时被两个引用所指向着. 所以, 对于这些同时被两个引用所指向的对象来说, 不论是用哪个引用来修改这些对象的值, 或者对他们进行增删, 都将影响到另外一个引用的指向结果.

先看这个内部类 ArrayList.SubList 的新增元素的方法 add(E e) . 由于在这个类内部没有找到这个签名的方法, 所以只能到他的父类中去找, 看下该类的继承关系:

private class SubList extends AbstractList<E> implements RandomAccess

在其父类 AbstractList 中找到了该方法的定义, 源码如下:

public boolean add(E e) {add(size(), e);return true;
}

该方法调用了 add(size(), e) 这个方法, 这个方法我们暂时先不分析, 留到后面分析. 先暂时做个记号, ——–标记0.
我们先分析 size() 方法, size() 方法在 AbstractList 类中没有找到, 我们先向上寻找, 即: 向他的父类中去找, 先看下 AbstractList 这个类的继承关系:

public abstract class AbstractList<E> extends AbstractCollection<E> implements List<E>

发现, AbstractList 的父类 AbstractCollection 中将 size() 定义为抽象方法, 所以, 我们只能向下去找, 也就是向 AbstractList 的子类, 即: 向本文分析的 ArrayList.SubList 这个内部类中去找, 我们在该内部类中找到了该方法的实现, 如下:

public int size() {checkForComodification();return this.size;
}

返回 this.size, 也就是 ArrayList.SubList 类中的 size字段的值, 而这个 size 字段其实在这个内部类的构造方法中就有赋值:

this.size = toIndex - fromIndex;

也就是对 listA 调用 subString() 方法时传入的两个索引值的差, 即: listB 中元素的总数.

好了, 我们绕的有点远, 我们再回到标记0处. 该分析 add(size(), e) 这个方法了. 这个方法在我们的内部类 ArrayList.SubList 中就有定义, 源码如下:

public void add(int index, E e) {rangeCheckForAdd(index);checkForComodification();parent.add(parentOffset + index, e);this.modCount = parent.modCount;this.size++;
}

第4行, 直接调用 parent 的 add() 方法, 也就是原 List ( listA ) 的 add() 方法, 该方法增加了偏移量 parentOffset, 并且 index 就等于 size() 的返回值, 而我们前边分析过, size() 的返回值就是 listB 中元素的总数. 我们这里做个记号以便后边回到这里继续分析——— 标记1.
这个 parentOffset 又是什么呢? 我们还是要看这个内部类 ArrayList.SubList 的构造方法:

SubList(AbstractList<E> parent,int offset, int fromIndex, int toIndex) {this.parent = parent;this.parentOffset = fromIndex;this.offset = offset + fromIndex;this.size = toIndex - fromIndex;this.modCount = ArrayList.this.modCount;
}

从第4行可知, parentOffset 就是 fromIndex, 而 fromIndex 其实就是我们创建子 List 时调用 ArrayListsubList(int fromIndex, int toIndex) 时为该方法中的 fromIndex 这个参数传入的值. 如果你不相信, 那就请再次回顾 subList(int fromIndex, int toIndex) 的源码吧:

public List<E> subList(int fromIndex, int toIndex) {subListRangeCheck(fromIndex, toIndex, size);return new SubList(this, 0, fromIndex, toIndex);
}

看第2行, 我们为 subList() 方法传入的 fromIndex, 作为 ArrayList.SubList 这个内部类的第三个参数, 而从该内部类的构造方法又可知, 这第三个参数最终会被赋值给该内部类的 parentOffset 字段, 也就是说, parentOffset 就是我们调用 subList() 方法获取子 List 时传入的起始坐标的值, 在我们这个例子中, 由于我们对 listA 调用 subList(2, 5) 获取到 listB, 所以, parentOffset 就是 2.

好了, 我们回到标记1处继续分析.

标记1处, 我们分析到了如下代码:

parent.add(parentOffset + index, e);

并且也知道了, 子 List (即: listB) 调用 add(E e) 方法, 其实最终是调用 parent.add(parentOffset + index, e) 方法的, 而我们前面分析过:

parentOffset = fromIndex
index = listB.size() = toIndex - fromIndex
parentOffset  + index = toIndex  // 也就是调用 subString()方法时, 所传入的第二个值

也就是调用 listA 的 add(toIndex, e) 方法, 而 toIndex 位置所指向的对象, 是同时被两个引用所指向, 所以, 如果调用 listB 的 add()方法向其中增加一个元素, 那么也必定会同时向 listA 中增加相同的元素, 因为从根本上来说, 这其实就是两个引用同时指向同一个对象嘛. 但是, 如果将这个过程反过来, 即: 向原 List (listA) 中增加一个对象, 那么将会抛出 ConcurrentModificationException 并发修改异常. 我们可以通过运行如下代码来得到证实:

public class SubListDemo {private static List list;private static List subList;public static void main(String[] args) {try {System.out.println("/*--------------------------- ArrayList -----------------------------------*/");subListTest(ArrayList.class);           } catch (Exception e) {e.printStackTrace();}}private static void subListTest(Class<? extends List> listClazz)throws IllegalAccessException, InstantiationException {if (listClazz == null) {throw new IllegalArgumentException(listClazz + " is null.");}list = listClazz.newInstance();list.clear();for (int i = 0; i < 5; i++) {list.add(i);}subList = list.subList(2, list.size());print();list.add(0, 10);System.out.println("---------此时在原list索引为0的位置上增加一个元素10, 同时将其他元素依次向后移动 ----------");print();}private static void print() {System.out.println("原 list: " + list);System.out.println("子 list: " + subList);}
}

得到的打印结果是:

这里写图片描述

List 的元素和原 List 中的后一部分是重合的, 而子 List 还在遍历过程中时, 向原 List 中新增元素, 这样给子 List 的遍历过程造成了干扰甚至困扰, 于是就抛出了并发修改异常. 同理, 我们也能合理推测出, 如果在遍历子 List 的过程中, 对原 List 执行的是删除元素的操作, 那么也必定会导致子 List 的遍历过程会抛出并发修改异常. 但是如果不是增删, 而是修改数值的操作, 就不会影响到子 List 的遍历过程, 所以就不会抛出并发修改异常.

我们还是简单看看这个内部类的 remove() 方法的源码吧, 如下:

public E remove(int index) {rangeCheck(index);checkForComodification();E result = parent.remove(parentOffset + index);this.modCount = parent.modCount;this.size--;return result;
}

看第4行, 还是调用了 parent 的 remove() 方法, 所以, 后续的分析完全和前面对 add() 方法的分析是同理的, 所以就不再分析了.

那我们再看看修改数值的方法, 也就是 set() 方法吧:

public E set(int index, E e) {rangeCheck(index);checkForComodification();E oldValue = ArrayList.this.elementData(offset + index);ArrayList.this.elementData[offset + index] = e;return oldValue;
}

第5行, 直接修改外部类 ArrayList 内部数组中相应元素的数值, 而由于子 List 使用的是原 List 的后一部分数据, 所以, 如果我们可以合理猜测, 如果此处修改的是数组中较为靠前的元素的数值, 那么只有原 List 中的数据会变化, 子 List 将不变. 而如果此处修改的是数组中较为靠后的元素的数值, 这个元素是被两个 List 中的元素共同指向着, 那么两个 List 中的数值将都会发生变化. 分析方法还是和分析 add() 方法同理.

其实, 我们可以继续修改上述代码, 来查看发生增删改查各自情况时的日志输出情况, 下面我对每种情况都分别进行一番实例测试, 将测试结果汇总成如下表格:

这里写图片描述

我们对该表格的测试结果进行总结, 可以得出如下结论:

这里写图片描述

这个结论对于我们日常的开发工作, 倒是起不到太大的帮助作用. 因为这些结论总结出的都是消极的结果, 而不是积极的结果. 不过, 这个结论倒是告诉我们:

如果你对一个 List 进行过 subList() 的操作之后,

1. 千万不要再对原 List 进行任何改动的操作(例如: 增删改), 查询和遍历倒是可以. 因为如果对原 List 进行了改动, 那么后续只要是涉及到子 List 的操作就一定会出问题. 而至于会出现什么问题呢? 具体来说就是:
(1) 如果是对原 List 进行修改 (即: 调用 set() 方法) 而不是增删, 那么子 List 的元素也可能会被修改 (这种情况下不会抛出并发修改异常).
(2) 如果是对原 List 进行增删, 那么此后只要操作了子 List , 就一定会抛出并发修改异常.

2. 千万不要直接对子 List 进行任何改动的操作(例如: 增删改), 但是查询和间接改动倒是可以. 不要对子 List 进行直接改动, 是因为如果在对子 List 进行直接改动之前, 原 List 已经被改动过, 那么此后在对子 List 进行直接改动的时候就会抛出并发修改异常.

既然获取子 List 后会有这么多限制条件, 一不小心就会出错, 那我们还怎么操作这个子 List 呢? 或者说, 怎样才能安全地操作子 List 呢? 其实, 你可能已经注意到了我在上述结论中提到的间接二字. 是的, 我们可以通过间接的方式来安全地操作子 List . 怎么间接呢? 其实, “间接” 和 “直接” 是相对的, 因为根据前边的分析, 子 List 会共用原 List 中后一部分的元素, 他们共同指向相同的对象, 这种共用对象的特性就是导致产生各种不安全结果的罪魁祸首. 如果我们将二者分别指向不同的对象, 岂不是就能避免不安全结果的产生? 也就是说, 我们需要让子 List 指向新的对象, 并且让新对象每个位置上的数值要和原 List 中相关位置上的数值相等即可. 于是就想到了以下两种间接的处理方式:

  1. 创建一个新的对象作为我们最终要操作的对象, 在其构造方法中, 将通过 subList() 方法获取到的子 List 作为该构造方法的参数传入. 这时, 这个新对象内所包含的元素和子 List 的完全相同, 但却指向的是不同的对象. 我们只需使用这个新创建的对象即可.
    对于 ArrayList

    List<Integer> subList = new ArrayList<>(list.subList(2, list.size()));  

    对于 LinkedList

    List<Integer> subList = new LinkedList<>(list.subList(2, list.size()));
  2. 创建一个新的对象作为我们最终要操作的对象, 然后调用这个新对象的 addAll() 方法, 将通过 subList() 方法获取到的子 List 作为 addAll() 方法的参数传入, 这时, 这个新对象内所包含的元素和子 List 的完全相同, 但却指向的是不同的对象. 我们只需使用这个新创建的对象即可.
    对于 ArrayList:

    List<Integer> subList = new ArrayList<>();  
    subList.addAll(list.subList(2, list.size())); 

    对于 LinkedList:

    List<Integer> subList = new LinkedList<>();  
    subList.addAll(list.subList(2, list.size()));  

    我们可以使用以上两种方式中的任意一种, 来解决我们在本文最开始遇到的那个 bug. 看如下代码:

public class SubListDemo {private static List list;private static List subList;public static void main(String[] args) {try {System.out.println("/*--------------------------- ArrayList -----------------------------------*/");subListTest(ArrayList.class);System.out.println("");System.out.println("/*--------------------------- LinkedList -----------------------------------*/");subListTest(LinkedList.class);} catch (Exception e) {e.printStackTrace();}}private static void subListTest(Class<? extends List> listClazz) throws IllegalAccessException, InstantiationException {if (listClazz == null) {throw new IllegalArgumentException(listClazz + " is null.");}list = listClazz.newInstance();list.clear();for (int i = 0; i < 5; i++) {list.add(i);}subList = listClazz.newInstance();List tempSubList = SubListDemo.list.subList(2, SubListDemo.list.size());subList.addAll(tempSubList);print();subList.add(5);System.out.println("---------此时将子list中增加一个元素 5 ----------");print();}private static void print() {System.out.println("原 list: " + list);System.out.println("子 list: " + subList);}
}

第28行, 我们为 subList 单独新建了一个对象, 让其指向这个新的对象. 然后在第30行, 调用 addAll() 将获取到的子 List 作为参数传入, 这样, subList 不仅指向了新的对象, 而且其内部的各个数值还和子 List 都是相同的. 运行结果如下:

这里写图片描述

我们发现, 为子 List 添加一个新元素5, 将不再影响原 List 了. 原 List 内的元素依然是 [0, 1, 2, 3, 4], 而不会再像先前的 bug那样也增加一个元素5了. 其他情况, 大家就自己测试吧.

好了, 这篇文章就到此为止. 通过本文的分析, 我们得出一个结论, 那就是, 经验主义有时会让你很受伤, 千万不要乱用经验主义. 本文所描述的主人公, 就是因为看到subList()subString() 这两个方法的命名方式类似, 于是根据经验主义而写出了错误的代码.


参考资料:

  • 《编写高质量代码 改善java程序的151个建议》 建议72 生成子列表后不要再操作原列表

http://chatgpt.dhexx.cn/article/8XGQWKU8.shtml

相关文章

【Java】List的subList方法

Java的容器类ArrayList很常用&#xff0c;旗下存在一个subList方法&#xff0c;是值得注意的。 subList方法仅能够取出此ArrayList的引用&#xff0c;即使其看起来&#xff0c;好像是取出一个ArrayList的子ArrayList。 其实不然&#xff0c;subList方法的返回值&#xff0c;只是…

Java中的subList方法

Java中的subList方法 今天看到了java中List中有个subList的方法&#xff0c;感觉很熟悉有没有&#xff1f;没错&#xff0c;在Stirng类中&#xff0c;也有个类似的方法&#xff1a;subString。 Stirng中的subString方法&#xff0c;官方解释是&#xff1a;返回字符串的子字符串…

Java中List集合的subList方法

目录 一、说明 二、测试 1、直接输出 2、向subList中添加元素再输出 3、 从subList中删除元素再输出 4、向list中添加元素再输出 5、从list中删除一个元素后再输出 ​ 6、向list中添加元素&#xff0c;输出list&#xff0c;然后将subList传入ArrayList生成新集合在输出…

你真的会用ArrayList的subList方法吗?

导语 在日常的开发中通常会遇到截取List的情况&#xff0c;而大多数会选择使用subList方法进行截取&#xff0c;但是好多人对这个方法的理解都只是停留在使用层面上&#xff1f;这篇文章会非常详细达到源码级别的讲解sublList方法&#xff0c;需要的朋友赶紧收藏起来吧。 关于…

Java SubList 类 Java subList方法 Java ArrayList$SubList 方法特点 SubList 用法

Java SubList 类 Java subList方法 Java ArrayList$SubList 方法特点 SubList 用法 一、概述 在java集合中&#xff0c;常用ArrayList类中&#xff0c;若需要对 list进行截取&#xff0c;可以使用subList方法&#xff0c;进行生成 SubList的内部类&#xff0c;那么 ArrayList 和…

使用ArrayList中的subList方法

集合是Java开发日常开发中经常会使用到的。在之前的一些文章中&#xff0c;我们介绍过一些关于使用集合类应该注意的事项&#xff0c;如《为什么阿里巴巴禁止在 foreach 循环里进行元素的 remove/add 操作》、《为什么阿里巴巴建议集合初始化时&#xff0c;指定集合容量大小》等…

sublist详解

接口中定义 List<E> subList(int fromIndex, int toIndex);1&#xff0c;该方法返回的是父list的一个视图&#xff0c;从fromIndex&#xff08;包含&#xff09;&#xff0c;到toIndex&#xff08;不包含&#xff09;。fromIndextoIndex 表示子list为空 2&#xff0c;父…

数据建模应用

数据建模应用 一、为什么要数据建模二、数据建模种类1、关系建模&#xff08;3NF&#xff09;2、维度建模 三、3NF数据建模1、范式介绍2、3NF建模实战 四、维度建模1、维度和指标的概念2、星型模型3、雪花模型4、星型与雪花模型对比5、维度建模测试案例 五、3NF建模与维度建模的…

分享大数据建模工具-大数据挖掘建模平台

大数据挖掘建模平台 是面向企业级用户的大数据挖掘建模平台。平台采用可视化操作方式&#xff0c;通过丰富内置算法&#xff0c;帮助用户快速、一站式地进行数据分析及挖掘建模&#xff0c;可应用于处理海量数据、高复杂性的数据挖掘任务&#xff0c;为其提供准确、高精度的计算…

大数据之数据模型

一、星型摸型 事实表是记录一个事实的&#xff0c;可以理解为订单表&#xff0c; 纬度表是提供更丰富信息的表&#xff0c;可以理解为商品明细表、订单明细表&#xff1b; 它是由一个事实表和一组维表组成&#xff0c;每个维表都有一个维作为主键&#xff0c;所有这些维的主键…

数据建模概述

数据建模&#xff08;data modeling&#xff0c;其实应该就是创建一个函数&#xff09;指的是对现实世界各类数据的抽象组织&#xff0c;确定数据库需管辖的范围、数据的组织形式等直至转化成现实的数据库。 将经过系统分析后抽象出来的概念模型转化为物理模型后&#xff0c;在…

大数据时代建模——图数据库建模

导读&#xff1a;云计算环境下&#xff0c;传统关系型数据库在海量数据存储方面存在瓶颈&#xff0c;对树形结构与半结构化数据的建模比较困难。本文介绍一种全新的建模方式——图数据库建模。应用图数据库模型更具扩展性、灵活性、高可靠性和高性能&#xff0c;能建立高细粒度…

大数据挖掘建模平台是怎样的?

大数据挖掘建模平台是可视化、一站式、高性能的数据挖掘与人工智能建模服务平台。面向企业级用户的大数据挖掘建模平台。平台采用可视化操作方式&#xff0c;通过丰富内置算法&#xff0c;帮助用户快速、一站式地进行数据分析及挖掘建模&#xff0c;可应用于处理海量数据、高复…

数据建模.

数据建模 什么是数据建模为什么要进行数据建模怎么进行数据建模 1. 为什么要进行数据建模&#xff1f; 提高 效率/性能&#xff1a; 计算机的的吞吐率&#xff0c;减少I/O的时间&#xff0c;提高用户使用数据的效率。开销&#xff1a;减少数据的冗余&#xff0c;从而节省存…

大数据数据建模

今天给大家分享一下 数据开发工作中数据建模的步骤&#xff0c; 第一步&#xff1a;选择模型或者自定义模型 这第一步需要我们基于业务问题&#xff0c;来决定我们需要选择哪种模型&#xff0c;目前市场中有很多模型可以供我们选择&#xff0c; 比如&#xff0c;如果要预测产…

数据建模

周三保(zhousbcn.ibm.com) IBM 软件部信息技术专家. 简介&#xff1a; 本文的主要内容不是介绍现有的比较流行的主要行业的一些数据模型&#xff0c;而是将笔者在数据仓库建设项目中的一些经验&#xff0c;在这里分享给大家。希望帮助大家在数据仓库项目建设中总结出一套能够合…

大数据分析及其建模利用

在数字经济时期&#xff0c;互联网、物联网、5G、大数据、智慧城市等各类模式的信息技术呈爆炸式增长&#xff0c;使得数据以令人难以设想的速度始终增长&#xff0c;企业运营的各个阶段都可以被记载下来&#xff0c;产品销售的各个环节也被记载下来&#xff0c;客户的生产行为…

浅谈大数据建模的主要技术:维度建模

文章目录 前言维度建模关键概念度量和环境事实和维度事实表维度表星形架构和雪花架构 维度建模一般过程1. 选取业务过程2. 定义粒度3. 确定维度4. 确定事实 前言 我们不管是基于 Hadoop 的数据仓库&#xff08;如 Hive &#xff09;&#xff0c;还是基于传统 MPP 架构的数据仓…

大数据建模五步法

from&#xff1a;https://www.sohu.com/a/198093510_783844 前一阵子&#xff0c;某网络公司发起了一个什么建模大赛&#xff0c;有个学员问我&#xff0c;数据建模怎么搞&#xff1f; 为了满足他的好学精神&#xff0c;我决定写这一篇文章&#xff0c;来描述一下数据分析必须…

大数据实践之数据建模

随着DT时代互联网、智能设备及其他信息技术的发展&#xff0c;数据爆发式增长&#xff0c;如何将这些数据进行有序、有结构地分类组织和存储是我们面临的一个挑战。 为什么需要数据建模 如果把数据看作图书馆里的书&#xff0c;我们希望看到它们在书架上分门别类地放置&#xf…