Generics and Lambdas

Example 1: Stack

Classes: Stack, Main

Stack

public class Stack<T> {
    protected class Node {
        public T value;
        public Node prev;

        public Node(T value, Node prev) {
            this.value = value;
            this.prev = prev;
        }
    }

    private Node top = null;

    public void push(T value) {
        top = new Node(value, top);
    }

    public T pop() {
        if (top == null)
            return null;
        else {
            Node temp = top;
            top = top.prev;
            return temp.value;
        }
    }

    public int size() {
        int count = 0;

        for(Node temp = top; temp != null; temp = temp.prev, count++);

        return count;
    }

    @Override
    public String toString() {
        String s = "-> ";
        Node temp = top;

        while (temp != null) {
            s = s.concat(temp.value.toString());
            temp = temp.prev;
            if (temp != null)
                s = s.concat(" - ");
        }

        return s;
    }
}

Main

public class Main {
    public static void main(String[] args) {
        // create a stack that can contain Strings (and/or descendants)
        Stack<String> strStack = new Stack<String>();
        strStack.push("Java");
        strStack.push("is");
        strStack.push("great");

        /*
        the following line won't compile due to type checking:
        strStack.push(10);
        */

        System.out.printf("String stack is of size %d and contents = %s.\n", strStack.size(), strStack);
        String topString = strStack.pop();
        System.out.printf("The topmost value of the string stack is '%s'.\n", topString);
        System.out.printf("String stack after popping = %s.\n", strStack);

        // create a stack that can contains Integers
        Stack<Integer> intStack = new Stack<Integer>();
        intStack.push(10);
        intStack.push(11);
        intStack.push(12);
        intStack.push(13);

        /*
        the following line won't compile due to type checking:
        intStack.push("hello");
        */

        System.out.printf("Integer stack is of size %d and contents = %s.\n", intStack.size(), intStack);
        int topInt = intStack.pop();
        System.out.printf("The topmost value of the string stack is '%s'.\n", topInt);
    }
}

Example 2: Key-Pair Class

Classes: Pair, Pairs, Main

Pair

/**
 * A class that contains a simple key-value pair.
 * @param <K> The type of a key object.
 * @param <V> The type of a value object.
 */
public class Pair<K, V> {
    private K key;
    private V value;

    public Pair(K key, V value) {
        this.key = key;
        this.value = value;
    }

    public K getKey() {
        return key;
    }

    public void setKey(K key) {
        this.key = key;
    }

    public V getValue() {
        return value;
    }

    public void setValue(V value) {
        this.value = value;
    }

    @Override
    public String toString() {
        return "(" + key + " = " + value + ")";
    }
}

Pairs

/**
 * A collection of key value pairs, where a value can be looked up using a unique key. This
 * is a <b>really</b> simple version of a dictionary/hash table. If you want a more complete
 * implementation, check out the Map interface and HashMap class.
 *
 * @param <K> The type of the keys.
 * @param <V> The type of the values.
 */
public class Pairs<K, V> {
    private ArrayList<Pair<K, V>> pairs;

    /**
     * Create an empty collection of pairs.
     */
    public Pairs() {
        pairs = new ArrayList<Pair<K, V>>();
    }

    /**
     * Is there a pair with the given key?
     *
     * @param key The key to be searched for.
     * @return true if the a pair exists with the given key, false otherwise.
     */
    public boolean contains(K key) {
        boolean found = false;

        for (Pair<K, V> pair : pairs) {
            if (pair.getKey().equals(key))
                found = true;
        }

        return found;
    }

    /**
     * Adds a new pair to the pairs, if there is not already a pair with the given key, otherwise updates the value associated with the key.
     *
     * @param key   The key of the pair.
     * @param value The value associated with the key.
     */
    public void set(K key, V value) {
        if (!contains(key)) {
            // doesn't contain the key, so set a new pairing
            pairs.add(new Pair<K, V>(key, value));
        } else {
            // key exists, so update the value paired with it
            for (Pair<K, V> pair : pairs) {
                if (pair.getKey().equals(key))
                    pair.setValue(value);
            }
        }
    }

    /**
     * Retrieves the value associated with the given key. If no value is found, then the default value is returned.
     *
     * @param key          The key being searched for.
     * @param defaultValue The value returned if there is no key-value pair.
     * @return The value associated with the key, if it exists, otherwise the default value.
     */
    public V get(K key, V defaultValue) {
        V value = defaultValue;

        for (Pair<K, V> pair : pairs) {
            if (pair.getKey().equals(key))
                value = pair.getValue();
        }

        return value;
    }

    @Override
    public String toString() {
        return pairs.toString();
    }
}

Main

public class Main {

    /**
     * Generates a collection of (Integer, V) pairs from a variable length list of parameters.
     * @param values The values to be added to the new pairings.
     * @param <V> The type of the pair value.
     * @return A pairs collection containing a collection of incrementally numbered pairs.
     */
    public static <V> Pairs<Integer, V> generate(V... values) {
        // create a new collection of pairs, where the key is of type Integer and the values are of unknown type.
        Pairs<Integer, V> pairs = new Pairs<>();

        Integer i = 0;
        // for every value passed in as a parameter
        for (V value : values) {
            // create a key, value pair
            pairs.set(i, value);
            i++;
        }

        return pairs;
    }

    public static void main(String[] args) {
        // Int -> String pairs
        Pairs<Integer, String> pairs = new Pairs<>();
        pairs.set(1, "one");
        pairs.set(2, "two");
        pairs.set(3, "three");
        pairs.set(1, "ONE");
        System.out.println("pairs = " + pairs);

        // note: no type casting needed!
        String value = pairs.get(1, "Unknown");
        System.out.println("value = " + value);

        value = pairs.get(10, "Unknown");
        System.out.println("value = " + value);

        // using the static method
        Pairs<Integer, Integer> fibs = generate(1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144);
        System.out.println("pairs = " + fibs);
        Integer f = fibs.get(10, -1);
        System.out.printf("f(10) = %d\n", f);
    }
}

Example 3: Lambdas

Classes: Example, Main, People, Person, PersonSelector

Person

public class Person {
    private String surname;
    private String firstName;
    private int age;

    public Person(String surname, String firstName, int age) {
        this.surname = surname;
        this.firstName = firstName;
        this.age = age;
    }

    public String getSurname() {
        return surname;
    }

    public void setSurname(String surname) {
        this.surname = surname;
    }

    public String getFirstName() {
        return firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return firstName + " " + surname + ", age " + age;
    }
}

Person Selector

public interface PersonSelector {
    public boolean isSelected(Person person);
}

People

public class People {
    private ArrayList<Person> people;

    public People() {
        people = new ArrayList<>();

        // add some dummy data
        people.add(new Person("Doe", "Jane", 17));
        people.add(new Person("Doe", "John", 21));
        people.add(new Person("Zee", "Jay", 45));
        people.add(new Person("Jay", "Dee", 30));
        people.add(new Person("Bee", "Jyujyu", 16));
        people.add(new Person("Duud", "Ald", 90));
    }

    public void displayYoungerThan25() {
        for(Person person : people)
            if(person.getAge() < 25)
                System.out.println(person);
    }

    public void displaySurnameStartsWithD() {
        for(Person person : people)
            if(person.getSurname().startsWith("D"))
                System.out.println(person);
    }

    public void displayYoungerThan25AndSurnameStartsWithD() {
        for(Person person : people)
            if((person.getAge() < 25) && (person.getSurname().startsWith("D")))
                System.out.println(person);
    }

    public void display(PersonSelector selector) {
        for(Person person : people)
            if(selector.isSelected(person))
                System.out.println(person);
    }
}

Main

public class Main {
    public static void main(String[] args) {
        People people = new People();

        /* selecting items to display using built in methods (not scalable!) */
        System.out.println("Starts with D (built-in)");
        people.displaySurnameStartsWithD();
        System.out.println();

        System.out.println("Younger than 25 (built-in)");
        people.displayYoungerThan25();
        System.out.println();

        System.out.println("Starts with D & younger than 25 (built-in)");
        people.displayYoungerThan25AndSurnameStartsWithD();
        System.out.println();

        ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
        // display selected people using an anonymous class - more scalable than built-in
        // just SO verbose for one method!
        System.out.println("Starts with D (anonymous)");
        people.display(new PersonSelector() {
            @Override
            public boolean isSelected(Person person) {
                return person.getSurname().startsWith("D");
            }
        });
        System.out.println();

        ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
        // display selected people using a named local class - even MORE verbose
        class SelectYoungerThan25 implements PersonSelector {
            @Override
            public boolean isSelected(Person person) {
                return person.getAge() < 25;
            }
        };
        // in this case can extend the named local class, since HAS a name
        // these classes ONLY exists within the main method, i.e. they cannot
        // be instantiated in another method
        class SelectStartsWithDAndYoungerThan25 extends SelectYoungerThan25 {
            @Override
            public boolean isSelected(Person person) {
                return super.isSelected(person) && person.getSurname().startsWith("D");
            }
        }

        System.out.println("Younger than 25 and starts with D (named local class)");
        people.display(new SelectStartsWithDAndYoungerThan25());
        System.out.println();

        ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
        // display selected people using a lambda expression
        System.out.println("Younger than 25 (lambda)");
        people.display(person -> person.getAge() < 25);
        System.out.println();

        // combine selectors
        PersonSelector selName = person -> person.getSurname().startsWith("D");
        PersonSelector selAge = person -> person.getAge() < 25;

        System.out.println("Starts with D & younger than 25 (lambda)");
        people.display(person -> selName.isSelected(person) && selAge.isSelected(person));
        System.out.println();

        // a lambda with a multi-instruction body
        // note the use of RETURN (not needed before)
        Example x = (n) -> {
            int count = 0;
            for(int i = 2; i <= n/2; i++)
                if((n % i) == 0)
                    count++;
            return count;
        };

        System.out.println("number of factors = " + x.count(100));
    }
}

**Example**
java @FunctionalInterface public interface Example { int count(int x); } ```

Example 4: Property Changed Listener

Classes: Person, Property, PropertyChangeListener, Main

Person

public class Person {
    public Property<String> name = new Property<>("Bob");
    public Property<Integer> age = new Property<>(20);
}

Property

public class Property<T> {
    private T value;
    private ArrayList<PropertyChangeListener<T>> listeners = null;

    public Property(T initialValue) {
        value = initialValue;
    }

    public void addListener(PropertyChangeListener<T> listener) {
        if(listeners == null)
            listeners = new ArrayList<>();

        listeners.add(listener);
    }

    public void removeListener(PropertyChangeListener<T> listener) {
        if(listeners != null)
            listeners.remove(listener);
    }

    public T get() {
        return value;
    }

    public void set(T newValue) {
        T oldValue = value;
        value = newValue;

        // notify listeners of changes
        if(listeners != null) {
            for(PropertyChangeListener<T> listener : listeners)
                listener.propertyChanged(this, oldValue, newValue);
        }
    }
}

PropertyChangeListener

@FunctionalInterface
public interface PropertyChangeListener<T> {
    void propertyChanged(Property<T> property, T oldValue, T newValue);
}

Main

public class Main {
    public static void main(String[] args) {
        new Main();
    }

    // note signature (if you ignore the name) matches the PropertyChangeListener<T> interface
    public void handleNameChanged(Property<String> property, String oldValue, String newValue) {
        System.out.println("Main.handle name change 1. Name = " + newValue);
    }

    public void handleNameChanged2(Property<String> property, String oldValue, String newValue) {
        System.out.println("Main.handle name change 2. Old name = " + oldValue + ", new name = " + newValue);
    }

    public void handleAgeChanged(Property<Integer> property, Integer oldValue, Integer newValue) {
        System.out.println("Main.handle age change. Age = " + newValue);
    }

    public static void staticHandleNameChanged(Property<String> property, String oldValue, String newValue) {
        System.out.println("Static.handle name change. Name = " + newValue);
    }

    public Main() {
        Person bob = new Person();

        // add Lambda expression listeners
        // attach listeners to bob's properties (note the types of the parameters)!
        // when typing, press CTRL+SHIFT+Space to be presented with basic code
        bob.name.addListener((property, oldValue, newValue) -> System.out.println("New name = " + newValue));
        bob.age.addListener((property, oldValue, newValue) -> System.out.println("New age = " + newValue));

        // just for fun, add a second lambda expression listener to be notified about name changes
        bob.name.addListener((property, oldValue, newValue) -> System.out.printf("Name was '%s', is now '%s'\n", oldValue, newValue));

        // using Lambdas, point to this object's methods that match the signatures
        bob.name.addListener(this::handleNameChanged);
        bob.name.addListener(this::handleNameChanged2);
        bob.age.addListener(this::handleAgeChanged);

        // using a static method
        bob.name.addListener(Main::staticHandleNameChanged);

        // now trigger the change listeners (note the parameter type)
        bob.name.set("Billy-Bob");
        // happy birthday Billy-Bob!!
        bob.age.set(21);
    }
}

Example 5: Method Call

Method Call

public interface MethodCall {
    void execute(SomeClass obj, int n);
}

Object Method Call

public interface ObjectMethodCall {
    void execute(int n);
}

SomeClass

public class SomeClass {
    private String name;

    public SomeClass(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return name;
    }

    public void f(int x) {
        System.out.printf("Calling f(%d) on %s\n", x, this);
    }

    public void g(int a) {
        System.out.printf("Calling g(%d) on %s\n", a, this);
    }

    public void h(int n) {
        System.out.printf("Calling h(%d) on %s\n", n, this);
    }
}

Main

public class Main {
    public static void main(String[] args) {
        // object instances
        SomeClass c1 = new SomeClass("c1");
        SomeClass c2 = new SomeClass("c2");

        // lambda expressions that call a specific execute on an object
        System.out.println("Calling a specific execute on different objects");
        MethodCall f = (obj, n) -> obj.f(n); // using "standard" way
        MethodCall g = SomeClass::g; // using "sugared" way

        // calling the same execute on different objects
        f.execute(c1, 10);  // call f(n) on c1
        f.execute(c2, 15);  // call f(n) on c2

        g.execute(c1, 5);   // call g(n) on c1
        g.execute(c2, 10);  // call g(n) on c2

        System.out.println();

        // call a specific execute on a specific object - note no object passed in as a parameter!
        System.out.println("Calling a specific execute on a specific object");
        ObjectMethodCall c1f = c1::f;
        ObjectMethodCall c2g = c2::g;
        ObjectMethodCall c1h = c1::h;

        c1f.execute(10);     // call c1.f()
        c2g.execute(20);     // call c2.g()
        c1h.execute(15);     // call c1.h()
    }
}