In order to work on objects inside collections in Java 8 it is common to use lambdas that do work on these items. Multiple lambdas can be applied to a collection in a sequential code style where the results of a lambda is passed to the next lambda. Depending on the used implementation not all results of a lambda has to be processed before these can be passed to the next one.
Demonstration
I experimented with an alternative approach for cases when the downsides reduce the legibility significantly. The basic idea is to take a collection and create a representative object. This object has the same type as an element of the collection.
One can interact with it as it was a single element of that collection. However, every interaction with that object is also applied onto all objects of the set in question. This object is not suited for retrieving information from the objects directly.
package net.splitcells.dem.merger.basic.experimental; import static org.mockito.Mockito.withSettings; import static org.powermock.api.mockito.PowerMockito.mock; import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.function.Function; import java.util.stream.IntStream; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; import lombok.val; /** * TODOC * */ public class ForEach { /** * * TODO Better configuration */ public static boolean IS_ONLY_STUBBING = new Boolean( System.getProperty("net.splitcells.den.merger.basic.experimental.ForEach.stubOnly", "true")); /** * TODOC * * FIXME Empty collections are currently not supported: * https://coderanch.com/t/383648/java/java-reflection-element-type-List */ @SuppressWarnings("unchecked") public static <T> T forEach(Collection<T> arg) { if (arg.isEmpty()) { throw new UnsupportedOperationException(); } if (IS_ONLY_STUBBING) { return (T) mock(arg.iterator().next().getClass(), withSettings().stubOnly().defaultAnswer(interceptor(arg))); } return (T) mock(arg.iterator().next().getClass(), interceptor(arg)); } /** * TODOC * * TEST */ private static <T> Answer<Object> interceptor(Collection<T> arg) { return new Answer<Object>() { @Override public Object answer(InvocationOnMock invocation) throws Throwable { List<Object> rVal = new ArrayList<>(arg.size()); for (T e : arg) { try { val method = e.getClass().getMethod(invocation.getMethod().getName(), convert(invocation.getArguments(), i -> i.getClass(), new Class<?>[invocation.getArguments().length])); Object rInvocation = method.invoke(e, invocation.getArguments()); if (rInvocation != null) { rVal.add(rInvocation); } } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) { throw new RuntimeException(ex); } } if (rVal.isEmpty()) { return null; } return forEach(rVal); } }; } /** * TEST * * TODO Move to not experimental part. */ private static <A, R> R[] convert(A[] from, Function<A, R> converter, R[] to) { assert from.length == to.length; IntStream.range(0, from.length).forEach(i -> to[i] = converter.apply(from[i])); return to; } }
When using this construct, 2 things have to be considered.
The current implementation currently only works on non empty collections in order to extract the type of the collection's elements.
Secondly, methods with primitive arguments are currently not supported which can be seen in the
- quote
testMethodsNotReturnThisViaNonPrimitiveParameters
Test.
package net.splitcells.dem.merger.basic.experimental; import static java.util.Arrays.asList; import static net.splitcells.dem.merger.basic.experimental.ForEach.forEach; import static org.assertj.core.api.Assertions.assertThat; import java.util.ArrayList; import java.util.List; import java.util.function.Consumer; import org.junit.Test; import org.junit.runner.RunWith; import org.powermock.core.classloader.annotations.PrepareForTest; import org.powermock.modules.junit4.PowerMockRunner; import lombok.val; /** * TODO Benchmark the effects that ForEach.IS_ONLY_STUBBING has on memory * consumption and runtime performance. * */ @RunWith(PowerMockRunner.class) @PrepareForTest({ ForEach.class, ForEachTest.class }) public class ForEachTest { private static final String RANDOM_STRING = "RANDOM_STRING"; private static final String RANDOM_STRING_CONCATINATION = RANDOM_STRING + RANDOM_STRING; /** * TODO File bug ticket for Powermockito/mockito: If the methods * testStaticOnlyStubOption and testStaticOnlyStubOption2 have the same name * than "java.lang.IllegalArgumentException: wrong number of arguments" is * thrown at attr * org.powermock.modules.junit4.internal.impl.PowerMockJUnit44RunnerDelegateImpl$PowerMockJUnit44MethodRunner.runTestMethod(PowerMockJUnit44RunnerDelegateImpl.java:326). */ @Test public void testStaticOnlyStubOption() { testThatForEachReturnsMock2(true); testThatForEachReturnsMock2(false); } public void testThatForEachReturnsMock2(boolean stubOnly) { ForEach.IS_ONLY_STUBBING = stubOnly; // By default a mock returns "" then toString() is invoked on that mock. assertThat(ForEach.forEach(asList(new StringBuffer(), new StringBuffer())).toString()).isEmpty(); } /** * FIXME Primitive parameter of get is causing problems. */ @Test public void testPrimitiveParameter() { List<List<StringBuffer>> testData = new ArrayList<>(asList(new ArrayList<>(), new ArrayList<>())); testData.get(0).add(new StringBuffer(RANDOM_STRING)); testData.get(1).add(new StringBuffer()); forEach(testData).get(0).append(RANDOM_STRING); assertThat(testData.get(0).toString()).isEqualTo(RANDOM_STRING_CONCATINATION); assertThat(testData.get(1).toString()).isEqualTo(RANDOM_STRING); } /** * An {@link List} implementation that has get method with no primitive * parameters. */ public static class ListI<T> extends ArrayList<T> { public ListI() { } public ListI(List<T> values) { super(values); } public T getValue(Integer index) { return get(index); } } @Test public void testMethodsNotReturnThisViaNonPrimitiveParameters() { ListI<ListI<StringBuffer>> testData = new ListI<>(asList(new ListI<>(), new ListI<>())); testData.get(0).add(new StringBuffer(RANDOM_STRING)); testData.get(1).add(new StringBuffer()); forEach(testData).getValue(0).append(RANDOM_STRING); assertThat(testData.getValue(0).getValue(0).toString()).isEqualTo(RANDOM_STRING_CONCATINATION); assertThat(testData.getValue(0).getValue(0).toString()).isEqualTo(RANDOM_STRING); } private static void appendRandomString(StringBuffer arg) { arg.append(RANDOM_STRING); } private static Consumer<StringBuffer> appendString(String string) { return i -> i.append(string); } /** * TODO Use an alternative to multiple string concatenation for better * demonstration. * * This test show cases alternatives. Note that this is not about the actual * StringBuffer operations. */ @Test public void testStandardLibraryClass() { val testSubjects = new ArrayList<>(asList(new StringBuffer(), new StringBuffer())); val controlGroup = new ArrayList<>(asList(new StringBuffer(), new StringBuffer())); val anotherControlGroup = new ArrayList<>(asList(new StringBuffer(), new StringBuffer())); val anotherOneControlGroup = new ArrayList<>(asList(new StringBuffer(), new StringBuffer())); { forEach(testSubjects).append(RANDOM_STRING).append(RANDOM_STRING); controlGroup.forEach(i -> i.append(RANDOM_STRING).append(RANDOM_STRING)); { anotherControlGroup.forEach(ForEachTest::appendRandomString); anotherControlGroup.forEach(ForEachTest::appendRandomString); } { anotherOneControlGroup.forEach(appendString(RANDOM_STRING)); anotherOneControlGroup.forEach(appendString(RANDOM_STRING)); } } controlGroup.stream().forEach(i -> assertThat(i.toString()).isEqualTo(RANDOM_STRING_CONCATINATION)); testSubjects.stream().forEach(i -> assertThat(i.toString()).isEqualTo(RANDOM_STRING_CONCATINATION)); anotherControlGroup.stream().forEach(i -> assertThat(i.toString()).isEqualTo(RANDOM_STRING_CONCATINATION)); anotherOneControlGroup.stream().forEach(i -> assertThat(i.toString()).isEqualTo(RANDOM_STRING_CONCATINATION)); } }
One major downside of this method is probably its performance.
This is currently not benchmarked or profiled.
Another downside of the currently implemented approach is that actions that are applied to a set of objects are only marked by a single entry point for each iteration level. This may especially reduce legibility when processing of collections by static methods is not that common.
Also, methods to manipulate single objects are used to manipulate a range of objects. This may come unexpected to the code reader.
At last the implemented approach does not work well for nested iterations because the static function
is not an
attribute of the collections types. Every nested usage of ForEach
causes an
additional
nesting in the AST of the source code. This would not be the case if ForEach
would be an
attribute of
the collections. In order to further increase the legibility of the code one could add an optional String
and/or Class argument
in order to identify the objects that are currently iterated. This argument would only be used for debugging
and logging purposes.
For normal execution this parameter would be ignored. With these improvements the values of a matrix could
be incremented this way: matrix.forEach("row").forEach("column").add(1)
.
Note that the additional parenthesis could be
omitted in alternative languages. An imaginary object oriented language that uses round brackets for String
constants and square brackets for lists could allow following syntax:
matrix.forEach(row).forEach(column).add[1]
.
An imaginary language could further remove syntax noise by omitting dots in places where it is clear
that an attribute is accessed:
matrix.forEach(row)forEach(column)add[1]
This style can create more readable and fluent code under some circumstances. It requires relatively much development resource in order to be implement effectively and efficiently. Additionally, it may have disadvantages if not used carefully and may be unexpected to new but experienced programmers.
In short a tool with high initial costs, that may easily be used in a confusing way and improves the code only a little. It also compensates an imperfect (but not bad) Stream API that could be enhanced in later versions of Java.
This approach may have merit on the whole but seems to solve a currently unimportant problem with an high amount of resources.