简体   繁体   中英

Why no tail() or head() method in List to get last or first element?

I recently had a discussion with a collegue why the List interface in Java doesn't have a head() and tail() method.

In order to implement such a functionality would have to write a wrapper that looked something like this:

public E head() {
 if (underlyingList == null || underlyingList.isEmpty())
  return null;

 return underlyingList.get(0);
}


public E tail() {
 if (underlyingList == null || underlyingList.isEmpty())
  return null;

 return underlyingList.get(underlyingList.size()-1);
}

I'm not aware of all List implementations but I assume that at least in LinkedList and ArrayList it should be pretty trivial to get the last and first element (constant time).

So the question is:

Is there a specific reason why it is not a good idea to provide a tail method to any List implementation?

The List inteface has subList which is almost head and tail . You can wrap it as follows

public List head(List list) {
    return list.subList(0, 1);
}

public List tail(List list) {
    return list.subList(1, list.size());
}

Edit

Following the answer by @Pablo Grisafi, here is a Java quick sort implementation - not generic and not efficient. As expected head() should return an element - not list.

public class QSort {

    public static List<Integer> qsort(List<Integer> list) {
        if (list.isEmpty()) {
            return list;
        } else {
            return merge(
                    qsort(lesser
                            (head(list), tail(list))),
                    head(list),
                    qsort(greater(
                            head(list), tail(list)))
            );
        }
    }

    private static Integer head(List<Integer> list) {
        return list.get(0);
    }

    private static List<Integer> tail(List<Integer> list) {
        return list.subList(1, list.size());
    }

    private static List<Integer> lesser(Integer p, List<Integer> list) {
        return list.stream().filter(i -> i < p).collect(toList());
    }

    private static List<Integer> greater(Integer p, List<Integer> list) {
        return list.stream().filter(i -> i >= p).collect(toList());
    }

    private static List<Integer> merge(List<Integer> lesser, Integer p, List<Integer> greater) {
        ArrayList list = new ArrayList(lesser);
        list.add(p);
        list.addAll(greater);

        return list;
    }

    public static void main(String[] args) {
        System.out.println(qsort(asList(7, 1, 2, 3, -1, 8, 4, 5, 6)));
    }
}

Java Collections Framework is written by Joshua Bloch. One of his API design principles is: High power-to-weight ratio .

tail() and head() can be implemented by get() and size() , so it's not necessary to add tail() and head() to a very general interface java.util.List . Once users use the methods, you don't have chance to remove them and you have to maintain these unnecessary methods forever. That's bad.

If you want to process a list recursively, which is often what head/tail are used for in functional programming, you can use an Iterator.

Integer min(Iterator<Integer> iterator) {
    if ( !iterator.hasNext() ) return null;
    Integer head = iterator.next();
    Integer minTail = min(iterator);
    return minTail == null ? head : Math.min(head, minTail);
}

As far as I can tell, List doesn't have an element method. LinkedList , however, has getFirst() and getLast() , which do as you describe.

In my humble opinion, tail and head are more familiar with people with a functional background. When you start passing functions around, they are incredible useful, that's why most functional languages have them implemented and even have shortcut notation to refer to them, like in haskell or even in scala (even if it's not THAT functional, I know)
In the "(almost) everything is an object but methods are made in a procedural way" java world, when passing around functions is at least hard and always awkward, head/tail methods are not so useful.
For example, check this haskell implementation of quicksort:

quicksort :: Ord a => [a] -> [a]
quicksort []     = []
quicksort (p:xs) = (quicksort lesser) ++ [p] ++ (quicksort greater)
    where
        lesser  = filter (< p) xs
        greater = filter (>= p) xs

It relies, among other things, on the ability to easily separate head and tail, but also on being able to filter a collection using a predicate. A java implementation (check http://www.vogella.de/articles/JavaAlgorithmsQuicksort/article.html ) looks totally different, it is way lower level, and doesn't rely on separating head and tail.
Note: The next sentence is totally subjective and based on my personal experience and may be proven wrong, but I think it's true:
Most algorithms in functional programming rely on head/tail, in procedural programming you rely on accessing an element in a given position

there are always choices one must make in good API design. there are lots of methods that could be added to the API, however, you have to find the fine line between making the API usable for most people and making it too cluttered and redundant. as it is, you can implement the tail method as you have shown in an efficient way for most List implementations, and LinkedList already has a getLast() method.

You should use a List, to make it easier. When you use recursion on a list you have to think like this... The list has an head(the first element) and a tail(all the other elements except the head). With recursion you need to do what you want on the head and then call the function on the tail, so that you always have a list with size = size - 1

public static void main(String[] args) {
    ArrayList<Integer> list = new ArrayList<>();
    
    list.add(11);
    list.add(12);
    list.add(0);
    list.add(3);
    list.add(1);
    list.add(4);
    list.add(11);
    
    System.out.println(countOccurrences(list, 11));
    
}



public static int countOccurrences(List<Integer> list, int n) {
    if (list.size() == 0) {//if list is empty return 0
        return 0;
    }else {
        if(list.get(0) == n) {//if head of list is equal to n add 1 and call the function on the tail of the list
            return 1 + countOccurrences(list.subList(1, list.size()), n);
        }else {//else if it's not equal to n call the function on the tail of the list without adding 1
            return countOccurrences(list.subList(1, list.size()), n);
        }
    }
}

You can get those on Stream which is available on list:

head

myList.stream().findFirst() // Optional<T>, return empty for empty list

tail (traditional meaning)

myList.stream().skip(1).collect(toList()) // or don't collect & continue with a Stream

last (possibly dangerous if the list is infinite:):

myList.stream().reduce((a,b) -> b) // Optional<T>, return empty for empty list

peekLast method is already defined in Deque interface.
Moreover, It's obligatory for deque to have such a functionality. So, there's no point to define it in List or any other interface.
It's just convenient to split the functionality. If you need random access then you should implement List . If you need to access the tail efficiently then you should implement Deque . You can easily implement both of them (LinkedList does this, actually).

head() is provided via list.iterator().next(), list.get(0), etc.

It is only reasonable to provide tail() if the list is doubly linked with a tail pointer, or based on an array, etc., Neither of these aspects is specified for the List interface itself. Otherwise it could have O(N) performance.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM