How do I add map and filter when I extend ArrayList in Java?
Asked Answered
S

2

8

I am trying to create a table class that extends ArrayList. In it, I would like to be able to create a map method that takes a lambda expression and returns a new table with the mapped values. I would also like to do this with filter. I use the map and filter a lot and I don't like typing out the whole thing over and over.

public abstract class Table<E extends Element> extends ArrayList<E> {
    // a lot of other stuff.

    public Table<E> map(/*WHAT DO I PUT HERE?*/ mapper) {
        return this.stream().map(mapper).collect(/*WHAT DO I PUT HERE?*/);
    }

    public Table<E> filter(/*WHAT DO I PUT HERE?*/ predicate) {
        return this.stream().filter(predicate).collect(/*WHAT DO I PUT HERE?*/);
    }
}

I am still trying to figure out generics. Maybe there is a better way. I don't know. I have tried duplicating what is in the original code for the ArrayList, but everything I try seems to create new problems.

Schiro answered 17/3, 2018 at 18:53 Comment(15)
Generally, for each new functionality you want to add to a class that doesn't know how to do it, you must provide it yourself. Basically, the places where you put WHAT DO I PUT HERE you should provide your new logic for the new functionality that you are adding.Bernina
Table is an abstract class, how would you collect into it?Skite
In your map function, what do you want to map to? As I understand, the source and result are a Streams of EEpinasty
@Andrew You could equally ask: List is an interface, how would Collectors.toList collect into it?Beet
Why is Table abstract? Is it your intention that the map and filter methods return an instance of the same class?Bricklaying
@AlexeyRomanov, Collectors.toList uses the ArrayList-based collector while here I don't see any subclass to provide default logicSkite
@cppbeginner Because Table has a lot of children and map should return the child table.Schiro
Ok well in that case neither of the solutions below are going to work. There is no way in java to express that map() always returns a instance of “this” class.Bricklaying
@cppbeginner There's no way to express it in the types, but my answer can actually provide this.Beet
@AlexeyRomanov I was only referring to types. I don’t think there’s any way to write collector() just once in the Table class that guarantees this for all subclasses.Bricklaying
@cppbeginner That's exactly why I made it abstract.Beet
Please refer to Efficient Java why extending ArrayList is not a good idea.University
@AlexeyRomanov Fair point, but I think the OP is hoping it can be done in Table. Given that it can’t l, I think it’s marginally better to make collector() final and instead have an abstract method with signature Table<E> newTable().Bricklaying
@cppbeginner Yes, that's a good alternative.Beet
Don't extend, compose. Write a class Table implements List and use ArrayList under the hood.Dante
B
8

On one hand, it's entirely possible:

public abstract class Table<E extends Element> extends ArrayList<E> {
    // implement in concrete subclasses
    public abstract <E1 extends Element> Collector<E1, ?, Table<E1>> collector();
    // Collector<E, ?, Table<E>> collector() is enough if map doesn't have E1 type parameter

    public <E1 extends Element> Table<E1> map(Function<E, E1> mapper) {
        return this.stream().map(mapper).collect(collector());
    }

    public Table<E> filter(Predicate<E> predicate) {
        return this.stream().filter(predicate).collect(collector());
    }
}

public class ChildTable<E extends Element> extends Table<E> {
    @Override
    public <E1 extends Element> Collector<E1, ?, Table<E1>> collector() {
        return Collectors.toCollection(() -> new ChildTable<E1>());
        // or simpler Collectors.toCollection(ChildTable::new);
    }
}

collector() could be implemented, but it would have to return a specific subtype of Table.

On the other, it may be better to have the list as a field of Table instead of extending it: prefer composition over inheritance. Do you really want all ArrayList methods available?

Beet answered 17/3, 2018 at 19:11 Comment(8)
I liked the idea, it looks like the template method patternSkite
yeah! templating pattern is really nice approach.Parados
In the child table, how would I implement collector()?Schiro
@MakPo E.g. Collector.toCollection(() -> new TableSubclass<E1>()).Beet
Collector doesn't have a toCollection() method. It says cannot find symbol.Schiro
@MakPo If your Table subclasses have fixed element types, then you can't use the version with E1 type parameter.Beet
@MakPo Collectors.toCollection, not Collector.Beet
Since it is only the supplier that differs, the design would be simpler when only declaring an abstract <E1> Supplier<Table<E1>> tableSupplier() method instead of collector(), to be implemented by the subclasses.Antoineantoinetta
P
3

Simply use Function<E, E> for mapper as your return type is same object and Predicate<E> for predicate as E is to be predicated. For Table whatever implementation class you have that you be used inside collect. See the way collect works.

It basically takes an initializer (first argument), BiFunction to specify how to add the element to the collection (second argument) and finally BiFunction to specify how it works in case of the parallel stream (third argument). See the code below:-

public abstract class Table<E extends Element> extends ArrayList<E> {
    public Table<E> map(Function<E, E> mapper) {
        return this.stream().map(mapper).collect(TableImpl::new, TableImpl::add, TableImpl::addAll);
    }

    public Table<E> filter(Predicate<E> predicate) {
        return this.stream().filter(predicate).collect(TableImpl::new, TableImpl::add, TableImpl::addAll);
    }
}

class TableImpl extends Table {
//whatever you impl is
}

Please feel free to extract the collect implementation to a Collector to avoid duplication of code.

Parados answered 17/3, 2018 at 19:16 Comment(5)
Or use a UnaryOperator<E> instead of Function<E, E>Epinasty
Nope! this is a generic impl. UnaryOperator will return the same object while OP expects generic impl of mapper. But if the operation is going to be unary then it could be passed as parameter while calling the map or filter as UnaryOperator extends Function. But good you asked it.Parados
You do realize Function<E, E> takes E and returns E which is what UnaryOperator<E> exactly does as well? UnaryOperator will return the same type of object not the same object which is also what Function<E, E> doesEpinasty
I don't understand what the TableImpl class is supposed to be or what I need to put in it.Schiro
It's just an example of what you need to write in your custom collector. You could extract it out to a method or a parameter of your method like another parameter in the method. I would recommend using a parameter to pass in the collector impl to be used.Parados

© 2022 - 2024 — McMap. All rights reserved.