3

I am trying to create a method that removes every Nth element from a List of unknown type (Wildcard), however every way I try doing it, it doesn't remove the specified elements, but I cannot figure out why. I have been struggling with this for two days now, so I am posting here as a last resort. I thank you in advance for any help.

The code I currently have is as follows:

public static void removeEveryNthElement(List<?> list, int n) {

    //Set list equal to an ArrayList because List is immutable
    list = new ArrayList<>(list);

    //If n is negative or zero throw an exception
    if(n <= 0) {
        throw new IllegalArgumentException("Integer n needs to be a positive number.");
    }

    //If the list is null, throw an exception
    if(list == null) {
        throw new NullPointerException("The list must not be null.");
    }

    //Remove every nth element in the list
    for(int i = 0; i < list.size(); i++) {
        if(i % n == 0) {
            list.remove(i);
        }
    }

The other way I have attempted is replacing the for loop with the following:

list.removeIf(i -> i % 3 == 0);

However, when I do it like this I receive the error that the % operator is undefined for the argument type. I have also tried using a for loop to individually add each element from the list into another modifiable list, but no matter what I do, I am having no luck. If you could help me with this it would be greatly appreciated!

3 Answers3

2

The most serious problem with your code is that removing an element at index i changes the index of all following elements and therefore your condition for removing elements (i % n) is wrong after removing the first element.

One way to get around the problem is iterating in reverse order:

for (int i = list.size()-1; i >= 0; i--) {
    if (i % n == 0) {
        list.remove(i);
    }
}

Another way is to increment i not by one, but by n and adjust it for the removed element:

for (int i = 0; i < list.size(); i += n) {
    list.remove(i);
    i--;
}

and, since i--; followed by i += n; is the same as i += n-1;:

for (int i = 0; i < list.size(); i += n-1) {
    list.remove(i);
}

An additional note: the check if (list == null) is useless after the statement list = new ArrayList<>(list);, since new ArrayList<>(list); already throws a NullPointerException if list is null

Thomas Kläger
  • 17,754
  • 3
  • 23
  • 34
  • This makes perfect sense, as I see how I didn't account for the leftward shift of the remaining elements. Nonetheless, my issue persists, as my code is not actually removing any elements (the list passed into the method remains the same as after the method is called) and I cannot figure out why – Brandon Bischoff Apr 30 '20 at 21:39
  • 1
    @BrandonBischoff sure, the list passed into the method is never changed - because you create a copy of that list with `list = new ArrayList<>(list);`. Only that copy is modified and you need to return that changed copy. – Thomas Kläger Apr 30 '20 at 21:47
  • The problem is the method is not allowed to have a return type. Furthermore, List is immutable, which is why I created a copy. I guess I just am blanking on how to update the original list to the modified list? – Brandon Bischoff Apr 30 '20 at 22:19
  • This ended up being the best answer. The only thing I had to modify was that it needed to remove i-1 instead. Furthermore, rather than creating a copy to modify, I just created an ArrayList in my test case rather than a List, therefore it would be mutable. Thank you much you helped me a lot – Brandon Bischoff Apr 30 '20 at 23:59
1

You need to keep in mind that creating new collection based on other collection is removing references to oryginal collection - values from collection are coppied to new one - any modification will not impact anything that is out of method scope. You'll need to pass the collection that supports deleting an object from itself or return new collection from method. Remember that the type does not define behaviour of object - its depending on the implementation that is compatible with class you cast to. Here is an example of what I said about the backend implementation (both variables are type List but the implementation is different).

Here is code when you want to do this 'in place':

public static void main(String[] args) {
    List<Integer> list2 = new ArrayList<>();
    list2.add(1);
    list2.add(2);
    list2.add(3);
    list2.add(4);

    removeEveryNthElement(list2, 3); // deleted number 3 because it is 3rd element
}

public static void removeEveryNthElement(List<?> list, int n) {
    for (int i = 2; i < list.size(); i += 3) {
        list.remove(i);
    }
}

But I would like to recommend not to do any operation that is not transparent to programmer. It's better to read and understand bigger programms when you know that you pass value to method and 'it does something' then get value back because it got changed. For this example I use generics and streams:

public static void main(String[] args) {
    List<Integer> list1 = Arrays.asList(1, 2, 3, 4);
    list1 = removeEveryNthElement2(list1, 3); //deleted number 3
    System.out.println();
}

public static <T> List<T> removeEveryNthElement2(List<T> list, int n) {
    final Predicate<T> p = new Predicate<T>() {
        int i = 0;

        @Override
        public boolean test(final T t) {
            return ++i % n != 0;
        }
    };

    return list.stream().filter(p).collect(Collectors.toList());
}
siwonpawel
  • 55
  • 1
  • 6
  • 1
    list.remove(i) returns an UnsupportedOperationException because a list is immutable, which is why I created a copy as an ArrayList in my method – Brandon Bischoff Apr 30 '20 at 22:33
  • Thats what I pointed out when mentioned the backend implementation. You could do the list mutable if you create ArrayList / LinkedList before passing it to method. list = new ArrayList(list); //change implementation to mutable list removeEveryNthElement(list, int n); //run your method – siwonpawel Apr 30 '20 at 22:52
  • This helped me the most, as the issue I was having was originating from the JUnit test case I was creating to test the method. Each time I was using the type List rather than an ArrayList etc and I could not figure out what to do differently. Not sure how I could overlook such a simple solution. Anyhow, I cannot thank you enough, your help was extremely appreciated – Brandon Bischoff May 01 '20 at 00:01
0

Brandon, let me first suggest that your method is side-effecting the list built in the calling method. This is allowed, but can lead to hard-to-understand bugs in more complicated code. Instead, try making a new list in your method, and assigned the returned value:

public class Remove {

    public static void main(String[] args) {
        List<String> list = Arrays.asList("a", "b", "c", "d", "e", "f", "g", "h");
        list = removeElements(list, 3);
        System.out.println(list);
    }

    public static <T> List<T> removeElements(List<T> list, int n) {
        List<T> newList = new ArrayList<T>();
        for (int i = 0; i < list.size(); i++) {
            if (i % n != 0) {
                newList.add(list.get(i));
            }
        }
        return newList;
    }
}

As a result, it makes the method simpler, because we're no longer iterating over the list being modified.

Have a look at Side effect--what's this? to read more about side-effecting.

Don Branson
  • 13,631
  • 10
  • 59
  • 101
  • Totally understand having a return; however I am not allowed to do this based on the requirements I was given. I am supposed to modify the original list passed into the method. This being said List is immutable so I had to make a copy. – Brandon Bischoff Apr 30 '20 at 22:21
  • Ah, homework? Maybe they're having you appreciate the evils of side-effects with a followup problem... – Don Branson Apr 30 '20 at 22:42
  • So, if you can't change it, and can't return it, that leaves...changing it and throwing it away? Maybe print it in the meantime. It'd be good to have the relevant parts of the calling method. – Don Branson Apr 30 '20 at 22:49