在Java中键入List vs type ArrayList

Type List vs type ArrayList in Java

1
2
3
(1) List<?> myList = new ArrayList<?>();

(2) ArrayList<?> myList = new ArrayList<?>();

我理解使用(1)可以交换列表接口的实现。似乎(1)通常用于应用程序中,而不管需要什么(我自己总是使用这个)。

我想知道是否有人使用(2)?

此外,这种情况实际需要使用(1)到(2)的频率(我能举一个例子吗)(即,除了对接口和最佳实践的编码之外,(2)还不够。)


几乎总是第一个优先于第二个。第一种方法的优点是,List的实现可以更改(例如,改为LinkedList),而不会影响代码的其余部分。这对于ArrayList来说是一项困难的任务,不仅因为您需要在任何地方将ArrayList更改为LinkedList,还因为您可能使用了ArrayList特定的方法。

您可以在这里阅读List实现。您可以从一个ArrayList开始,但很快就会发现另一个实现更合适。


I am wondering if anyone uses (2)?

对。但很少有合理的理由(IMO)。

当人们本应使用List时,他们使用ArrayList会被烧死:

  • Collections.singletonList(...)Arrays.asList(...)这样的实用方法不会返回ArrayList

  • ListAPI中的方法不保证返回相同类型的列表。

例如,在https://stackoverflow.com/a/1481123/139985中,由于ArrayList.sublist(...)不返回ArrayList,海报出现了"切片"问题。他设计的代码使用ArrayList作为所有列表变量的类型。他把子列表复制到一个新的ArrayList中,最终"解决"了这个问题。

您需要了解List的行为的论点主要是通过使用RandomAccess标记接口来解决的。是的,它有点笨重,但另一种情况更糟。

Also, how often does the situation actually require using (1) over (2) (i.e. where (2) wouldn't suffice..aside 'coding to interfaces' and best practices etc.)

问题的"多久"部分在客观上是无法回答的。

(and can I please get an example)

有时,应用程序可能要求您在ArrayListAPI中使用不在ListAPI中的方法。例如,ensureCapacity(int)trimToSize()removeRange(int, int)。(最后一个只会出现在您创建了一个arraylist的子类型,该子类型声明了方法为public)。

这是编码到类而不是接口IMO的唯一合理原因。

(理论上,你的表现可能会略有改善……在某些情况下…在某些平台上…但除非你真的需要最后的0.05%,否则这样做是不值得的。这不是一个合理的理由,依我看。)

You can’t write efficient code if you don’t know whether random access is efficient or not.

这是一个有效的观点。然而,Java提供了更好的处理方法;

1
2
3
public <T extends List & RandomAccess> void test(T list) {
    // do stuff
}

如果用一个不实现RandomAccess的列表来调用它,您将得到一个编译错误。

您还可以动态测试…使用instanceof…如果静态输入太笨拙。您甚至可以编写代码来使用不同的算法(动态地),这取决于列表是否支持随机访问。

注意,ArrayList不是实现RandomAccess的唯一列表类。其他包括CopyOnWriteListStackVector

我看到人们对Serializable也有同样的看法(因为List没有实现它)。但上述方法也解决了这个问题。(在某种程度上,它可以使用运行时类型来解决。如果任何元素不可序列化,则ArrayList将无法序列化。)


例如,您可能认为LinkedList是您的应用程序的最佳选择,但后来决定,出于性能原因,ArrayList可能是更好的选择。

用途:

1
List list = new ArrayList(100); // will be better also to set the initial capacity of a collection

而不是:

1
ArrayList list = new ArrayList();

供参考:

enter image description here

(主要发布用于收集图表)


在类型集变量中存储对HashSetTreeSet的引用被认为是一种好的样式。

Set names = new HashSet();

这样,如果您决定使用TreeSet,则只需更改一行。

此外,对集合进行操作的方法应指定集合类型的参数:

public static void print(Set s)

然后该方法可以用于所有集合实现。

理论上,我们应该对链表提出同样的建议,即保存类型列表变量中的LinkedList引用。然而,在Java库中,列表接口对于EDCOX1、0和EDCOX1 9个类都是通用的。特别是,它有用于随机访问的get和set方法,即使这些方法对于链表效率非常低。

如果不知道随机访问是否有效,就不能编写有效的代码。

这显然是标准库中的严重设计错误,我不建议使用因为这个原因,列表接口。

看看这个错误有多尴尬collections类的binarySearch方法的源代码。这个方法需要列表参数,但二进制搜索对于链接列表没有意义。然后代码笨拙地尝试发现列表是否为链接列表,然后切换到线性搜索!

Set接口和Map接口设计得很好,您应该使用它们。


如果代码是列表的"所有者",则使用(2)。例如,对于局部变量,这是正确的。没有理由使用抽象类型List而不是ArrayList。另一个证明所有权的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Test {

    // This object is the owner of strings, so use the concrete type.
    private final ArrayList<String> strings = new ArrayList<>();

    // This object uses the argument but doesn't own it, so use abstract type.
    public void addStrings(List<String> add) {
        strings.addAll(add);
    }

    // Here we return the list but we do not give ownership away, so use abstract type. This also allows to create optionally an unmodifiable list.
    public List<String> getStrings() {
        return Collections.unmodifiableList(strings);
    }

    // Here we create a new list and give ownership to the caller. Use concrete type.
    public ArrayList<String> getStringsCopy() {
        return new ArrayList<>(strings);
    }
}


当您编写List时,实际上您告诉我们,您的对象只实现了List接口,但没有指定您的对象属于哪个类。

编写ArrayList时,指定对象类是一个可调整大小的数组。

因此,第一个版本使您的代码在将来更加灵活。

查看Java文档:

ArrayListList接口的可调整大小的数组实现。

接口List—有序集合(也称为序列)。这个界面的用户可以精确控制每个元素在列表中的插入位置。

Array容器对象,具有固定数量的单一类型值。


事实上,有时(2)不仅是首选,而且是强制性的,我很惊讶,这里没有人提到这一点。

序列化!

如果您有一个可序列化类,并且希望它包含一个列表,那么您必须声明该字段是一个具体的、可序列化的类型,如ArrayList,因为List接口不扩展java.io.Serializable

显然,大多数人不需要序列化,而忘记了这一点。

一个例子:

1
2
3
4
public class ExampleData implements java.io.Serializable {

// The following also guarantees that strings is always an ArrayList.
private final ArrayList<String> strings = new ArrayList<>();


我认为使用(2)的人不知道Liskov替换原则或依赖倒置原则。或者他们真的必须使用ArrayList


(3)collection myCollection=new arraylist();

我通常使用这个。只有当我需要列表方法时,我才会使用列表。与arraylist相同。您总是可以切换到更"窄"的界面,但不能切换到更"宽"的界面。


以下两项中的一项:

1
2
(1) List<?> myList = new ArrayList<?>();
(2) ArrayList<?> myList = new ArrayList<?>();

通常首选第一种。由于您将只使用来自List接口的方法,因此它为您提供了将来使用List的一些其他实现的自由,例如LinkedList。所以它将您与特定的实现分离。现在有两点值得一提:

  • 我们应该总是按接口编程。这里更多。
  • 您将几乎总是使用ArrayList而不是LinkedList。这里更多。
  • I am wondering if anyone uses (2)

    是的,有时(很少阅读)。当我们需要的方法是ArrayList实现的一部分,而不是接口List的一部分时。例如ensureCapacity

    Also, how often (and can I please get an example) does the situation
    actually require using (1) over (2)

    几乎总是你喜欢选项(1)。这是OOP中的一个经典设计模式,您总是试图将代码从特定的实现和程序分离到接口。


    有人再次问这个问题(副本),这让我在这个问题上做得更深入了。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    public static void main(String[] args) {
        List<String> list = new ArrayList<String>();
        list.add("a");
        list.add("b");

        ArrayList<String> aList = new ArrayList<String>();
        aList.add("a");
        aList.add("b");

    }

    如果我们使用字节码查看器(我使用的是http://asm.ow2.org/eclipse/index.html),我们会怎么做?我看到下面的列表片段(仅列表初始化和分配):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
       L0
        LINENUMBER 9 L0
        NEW ArrayList
        DUP
        INVOKESPECIAL ArrayList.<init> () : void
        ASTORE 1
       L1
        LINENUMBER 10 L1
        ALOAD 1: list
        LDC"a"
        INVOKEINTERFACE List.add (Object) : boolean
        POP
       L2
        LINENUMBER 11 L2
        ALOAD 1: list
        LDC"b"
        INVOKEINTERFACE List.add (Object) : boolean
        POP

    对于主义者:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
       L3
        LINENUMBER 13 L3
        NEW java/util/ArrayList
        DUP
        INVOKESPECIAL java/util/ArrayList.<init> ()V
        ASTORE 2
       L4
        LINENUMBER 14 L4
        ALOAD 2
        LDC"a"
        INVOKEVIRTUAL java/util/ArrayList.add (Ljava/lang/Object;)Z
        POP
       L5
        LINENUMBER 15 L5
        ALOAD 2
        LDC"b"
        INVOKEVIRTUAL java/util/ArrayList.add (Ljava/lang/Object;)Z
        POP

    区别在于列表最终调用invokeInterface,而alist调用invokeVirtual。根据bycode大纲插件引用,

    invokeinterface is used to invoke a method declared within a Java
    interface

    调用虚拟机时

    invokes all methods except interface methods (which use
    invokeinterface), static methods (which use invokestatic), and the few
    special cases handled by invokespecial.

    总之,invokeVirtual在invokeInterface中从堆栈中弹出ObjectRef

    the interpreter pops 'n' items off the operand stack, where 'n' is an 8-bit unsigned
    integer parameter taken from the bytecode. The first of these items is
    objectref, a reference to the object whose method is being called.

    如果我正确理解这一点,那么区别主要在于每种方法如何检索objectref。


    列表是一个接口。它没有方法。在列表引用上调用方法时。实际上,它在这两种情况下都调用arraylist方法。

    以后可以把List obj = new ArrayList<>改为List obj = new LinkList<>或其它实现列表接口的类型。


    我唯一知道(2)更好的地方是在使用GWT时,因为它减少了应用程序的占地面积(不是我的想法,但是GoogleWebToolkit团队这么说)。但是对于JavaM(1)内部运行的普通Java可能总是更好。


    List接口有几个不同的类——ArrayListLinkedListLinkedList用于创建索引集合,ArrayList用于创建排序列表。因此,您可以在参数中使用其中的任何一个,但是您可以允许使用您的代码、库等的其他开发人员使用不同类型的列表,不仅是您使用的列表,所以,在这个方法中

    1
    2
    3
    ArrayList<Object> myMethod (ArrayList<Object> input) {
       // body
    }

    您只能将它与ArrayList一起使用,而不能与LinkedList一起使用,但是您可以允许在它方法正在使用的其他地方使用List类中的任何一个,这只是您的选择,因此使用接口可以允许它:

    1
    2
    3
    List<Object> myMethod (List<Object> input) {
       // body
    }

    在这个方法参数中,您可以使用任何您想要使用的List类:

    1
    2
    3
    4
    5
    List<Object> list = new ArrayList<Object> ();

    list.add ("string");

    myMethod (list);

    结论:

    尽可能在任何地方使用接口,不要限制您或其他人使用他们想要使用的不同方法。


    我想说1是首选,除非

    • 您依赖于arraylist中可选行为*的实现,在这种情况下,显式地使用arraylist更清楚
    • 您将在需要arraylist的方法调用中使用arraylist,可能用于可选的行为或性能特征。

    我的猜测是,在99%的情况下,你可以通过列表,这是首选。

    • 例如,removeAlladd(null)