Jump to content

Polymorphic's Introduction To Lambdas and Streams Tutorial


Recommended Posts

Polymorphic - Lambdas and Streams Tutorial

The way you think about Java programming is about to change forever. Before Java SE 8, Java supported three types of programming; procedural programming, object-oriented programming and generic programming. Java SE 8 added a fourth type of programming, functional programming, lambdas and streams are critical technologies behind functional programming. 

Why are lambdas and streams important?

1) Functional programming techniques allow you to write higher-level code, because most of the details are implemented for you by Java. Your code becomes concise, which improves productivity and can help you rapidly prototype programs.

2) Functional programming techniques eliminate large classes of errors, such as off-by-one errors, incorrectly modifying variables and wrong loop continuation conditions. 

What will I learn from this tutorial?

- You will learn functional programming techniques.

- You will learn lambdas and streams.

- You will learn what streams are and how stream pipelines form. 

- You will learn intermediate operations and terminal operations. 

- You will perform intermediate operations such as filter and map on streams.

- You will perform terminal operations such as anyMatch, count, forEach, findFirst and sum on streams.

- You will learn about functional interfaces and Tribot API.

Part One - The Basics: Data Source, Processing Steps, Intermediate/Terminal Operations, Lambdas, and Streams

Before you were used to counter-controlled iteration, specifying what you want to accomplish and how to accomplish it with a for loop.

RSItem[] inventoryItems = Inventory.getAll();
int totalGold = 0;

for (int i = 0; i <= inventoryItems.length; i++) {
   RSItemDefinition itemDefinition = inventoryItems[i].getDefinition();
   if (itemDefinition != null) {
     totalGold += itemDefinition.getValue();
   }
   
}

General.println(totalGold);

The above is an example of external iteration because all of the details are expressed - sum all the inventory item’s gold value (you must null check the itemDefinition). 

The problem with this example is that it's error prone. The code mutates, everytime code that modifies a variable could easily introduce an error. 

Firstly, you could initialize the totalGold variable incorrectly.

Secondly, you could initialize the for loop control variable incorrectly.

Thirdly, you could use the incorrect loop continuation condition.

Fourthly, you could increment the control variable incorrectly.

Lastly, you could add each RSItem gold value to totalGold incorrectly.

Now let’s try a different approach, instead of specifying how to do it, specify only what to do. 

int totalGold = Arrays.stream(Inventory.getAll())
  .map(RSItem::getDefinition)
  .filter(Objects::nonNull)
  .mapToInt(RSItemDefinition::getValue)
  .sum();

Notice that there is neither a counter controlled variable nor a variable to cache the total gold total, this is because the stream conveniently defines the data source and sum. 

There are 5 chained method calls; stream(), map(), filter(), mapToInt(), and sum(). 

The chained method calls create a stream pipeline. A stream is a sequence of elements on which you perform tasks, and the stream pipeline moves the stream’s elements through a sequence of processing steps. 

Good programming practice indicates that when using chained method calls, align the dots vertically for readability as I did in the previous example. 

A stream pipeline usually begins with a method call that creates the stream, known as the data source, Arrays.stream(Inventory.getAll()) is the data source method call.

Arrays.stream(Inventory.getAll()) produces a stream containing the ordered sequence of inventory items. Next, the intermediate operation map() performs a processing step that maps each RSItem's definition to the current stream element. The intermediate operation filter() null checks the current stream element (RSItemDefinition). The intermediate operation mapToInt() performs a processing step that maps each element in the stream to that item’s gold value by passing a lambda expression. 

A lambda is an anonymous method, a method without a name, and many stream operations receive methods as arguments. The example lambda expression can be read as “a method that receives an RSItemDefinition parameter value and returns that RSItemDefinition’s gold value.” For each element in this stream mapToInt() calls this method, passing to it the current stream element. The method’s return value becomes part of the new stream that mapToInt() returns. 

Finally the terminal operation sum() produces a single result, the sum of the stream’s elements. This is known as a reduction, because it reduces the stream of values to a single value (the sum). 

A very common intermediate operation called filter() returns a stream containing only the elements that satisfy a condition, known as a predicate. As a result, the new stream usually has fewer elements than the original stream.

Now, let’s find all the items in the inventory that are worth more than 4000 gold and sum the results. 

// Fixed by JustJ: Do the map first, then we don't have to handle it twice
int totalGold = Arrays.stream(Inventory.getAll())
  .map(RSItem::getDefinition)
  .filter(rsItemDefinition -> Objects.nonNull(rsItemDefinition) && rsItemDefinition.getValue() > 4000)
  .mapToInt(RSItemDefinition::getValue)
  .sum();

  General.println(totalGold);

The stream pipeline performs five method calls. Creating the data source, then mapping the RSItemDefinition to the current stream element. Next, filtering out the null elements producing a stream of the items gold that are greater than 4000. Finally, reducing the stream to the sum of its elements.

 

Part Two - A Deeper Look: Stream Pipelines, Lambdas, and Method References

Now that you understand the structure of a stream and its importance. Let’s take a deeper look at how the elements move through the stream pipeline.

In part one we have initiated stream pipelines with a terminal operation called sum(). The intermediate operations processing steps such as filter() and mapToInt() are applied for a given stream element before they are applied to the next stream element. 

In other words, filter() must return true, to invoke mapToInt() - sequentially. 

    For each RSItem

        If the RSItem gold value is greater than 4000

            Map the gold value to the current stream element and add the result to the gold total

To prove this, consider this modified version of part one’s previous stream pipeline, in which each lambda displays the intermediate operations name and the current stream elements’ gold value.

// Assume return value RSItemDefinition is not null for this example only, for demonstration purposes.
// Note: If you do not null check the definition, your script could break.

int totalGold =
       Arrays.stream(Inventory.getAll())
               .filter(rsItem -> {
                   int gold = rsItem.getDefinition().getValue();
                   General.println("filter: " + gold);
                   return gold > 4000;
               })
               .mapToInt(value -> {
                   int gold = value.getDefinition().getValue();
                   General.println("mapToInt: " + gold);
                   return gold;
               })
               .sum();

As you can see, the modified pipeline’s output clearly displays that each item’s mapToInt step is applied before the next stream element’s filter step (Assume the inventory contains a rune axe and maple log):

dVBrvs0a--DAR4T3zGwyW3_4fefbbVJ54PXaUqcvIZ3-1th2CLfBGk9vXChyjnuPJPx1z8dYXBIScLZF23lwAs4Cz_4I2MOCnGwwl3ZKXk1pP4LiTWXV81Aju9zdeTNTO7vXL0NlAAzt0a2XtAGHMPGc3zraSslDJRFLWNbjV33Mi8NIj6YYrc8DPwspYIz8ZXKzaZ_J8zXWIaVvoK_siBY85YQ1YUOIAHI4kLOLPdSIRtrVogWJsri1Fq41uTfM2qk3CKr7OkT4G-aH

The maple log isn’t mapped because it doesn't satisfy the predicate (filter).

Before moving on I would like to point out a much cleaner way of performing this:

// By JustJ:
// There's a great function called .peek which can be used here to make this cleaner
// I'm aware you're doing this as an example, but .peek is great for checking the state between steps

int totalGold = Arrays.stream(Inventory.getAll())
    .mapToInt(rsItem -> rsItem.getDefinition().getValue())
    .peek(value -> General.println("Gold: " + value))
    .filter(gold -> gold > 4000)
    .sum();

Now, let’s take a look at this example, you have already seen this before, known as method references. For any lambda that simply calls another method, you can replace the lambda with that method’s name.

Arrays.stream(Banking.getAll())
  .map(RSItem::getDefinition)
  .filter(Objects::nonNull)
  .map(RSItemDefinition::getName)
  .forEach(General::println);

First we created the data source, all the bank items (assume the bank is open). Next, making use of the map intermediate operation, returning a stream of all the item names. Finally, calling the forEach terminal operation, using the method reference to print all the names to the client debug. 

The compiler converted the method reference - General::println into an appropriate lambda expression. The shorthand notation used above, calls the specific method to perform on the parameter’s value. Which in this case is to print all the item names’ inside the bank.   

kwkxEVdr5lvPm-tdwr33iacaxhUbGOvqlAkXVXRQmyHnnNa0wVE9Qg6UMR2_laEglvVOUDsBfmaE_r3g7YZGP0H33AUDXtQUGSy4tooeMsX78G3bxgZV6CTJTP330yQqLcjjIBM0rWMgjbxTlocWJ3ORFI05HKi1qCD1_IkgncgyD0ulZI5uH0uOLg00rKFqyptsXyY-zu-TEGZh_5_hKZYXE8B7WjpXkvs1Yz536q0qRDH2GVOJDFrggaIuwbREb1z8Y65bLaSlKPqI

 

Part Three - Generic Functional Interfaces, Creating and Manipulating stream of RSObject, Creating and Manipulating a stream of RSPlayer

Java SE 8 introduced enhanced interface features. Such as default methods, static methods, and functional interfaces. A functional interface is an interface that contains only one abstract method (could contain default and static methods). Also known as single abstract method interfaces (SAM), they are used frequently in Functional Java Programming. 

So far we have been working with pure functions, which have referential transparency, meaning:

1) Depend only on the parameters. 

2) Have no side effects.

3) Do not maintain any state.

Lambdas are defined as pure functions, methods that implement functional interfaces. Like those that have been used so far. State changes occur by passing data from method to method, no data is cached whatsoever. 

It’s widely known that pure functions (lambdas) are safer. They don’t modify the program’s state, which makes them less error prone and easier to debug.

In part two we passed a lambda to the method forEach, for displaying all bank item names. The forEach method represents a one parameter function called Consumer<T>, of generic type, that returns void. 

iWUr0uXptML5ISdJPLKjuwKxj6ETlK6BTeF1zNoaXQuSZZ0rALcQ9DGVWMXGKoNElTPSJGNIOMNtbdjB6hTIbQYwwiif1ns6w0Mg8wjod2totg9IRZaRDHXnd2lVQnueZZ5Hgpp6

YN0TACFcc051GGiEUQ8WNQKYExPVf3m_iIlSXxmlSW2unLLVulzHHOP0OoZEMYIP6_Z3OjDCo8J6LzH9zDZQ6q7XMaTGQI8jQbXnojHePnqFHt7gHHRlWWIqo9AdhQX9F3N5D_Gq

The Java documentation clearly outlines that the Consumer interface is a functional interface. That is, containing a single abstract method.

Now let’s take a look at the Tribot API, as you navigate through the API you will notice that some classes are deprecated. Notably the Filter and Condition class, the developers now want us to use BooleanSupplier and Predicate functional interfaces instead. 

XZPnOXkGZ2tvsR5pFnjXtErf95Pi-PctUbfylBiUA0m_L3sQP4JMjAyXcFAAAiJHrAFWSRPPMlv4EUaWERBbO-300QHiVDGMUAQz5KyVgXMCG9lYZXJJUZccLGrbkIYGyDAsxqaI

In part one we used the filter method, known as a predicate, returning a new stream containing all the inventory items worth more than 4000 gold. 

6ctbYetq78ULGqyNiUbOLCFC-5yHXUg9mMZJa7xXS3g8i8zf9tOwCs7G0D4vCkOMT1UIgvMseclV5AeOoF4Hk57CbhaHzQubyTEFTJdXCL6z3NU3tv1WJpkwX_gJi8zv3wSMGBqf

NSbfq_e0-gdYKrsVwFcLmaAbC73tmFpR0Ifert5ED6WDvncZsZ38-UvaszhFkbCIi0BN3i2zL7dXqGQzJWWxVqxem9RphjnN3IcEjwJ4pIkcFeek-z0hhDn2n6lNx3qWNwGM3Pwu

A predicate represents a one parameter method that returns a boolean result. The lambda we passed to the method filter(), determines whether the parameter satisfies a condition (is the item worth more than 4000 gold).

Let’s talk about the Tribot API more, the Timing class is a critical class for any script. The Timing class makes use of BooleanSupplier and Predicate functional interfaces. 

Now, let’s take a look at this example demonstrating the Timing class methods and the BooleanSupplier functional interface. 

// Wait for 2 seconds or until the player is animating.
boolean result = Timing.waitCondition(() -> Player.getAnimation() != -1, 2000);

According to the Tribot API documentation, this method waits for a certain condition to become active. That is, wait for the player to start working/animating, for this lambda. The bot will essentially become frozen (or sleep) for at least two seconds or until the player starts working/animating. If the player doesn’t start working within the timeout, the method returns false. 

rLOvZBzpX9eZ6SiZm_ninGS2EtAIMJJlbL8LixmeL0PNe01DYay0EUQREajsHw78a6nwWxG8NMkjBcwyzhlLlrFtAECkqEARCTROFsMZUrS8VWQAamhHzt5Z0hZG9eeQul0Nf_cH

NALUd-3XY5pAIhqXM21V8dTVjUP7WS4yGxGCMd-QKqE4BneAEBJMtcpDSMlloqDsxfghB18vXYEVMqXjnFjV513dsnLeUSEzcmUDsWrdYZEDci0cVp1Abz2xBmZSofnS-ZuCK1cU

Now we’re going to create a stream of RSObject’s that are trees, clickable, and on screen (the predicate or filter). In addition, the data source will only occur if the RSObject’s are within the distance of 5 to the player.

long count = Arrays.stream(Objects.findNearest(5, Constants.IDs.Objects.trees_normal))
  .filter(rsObject -> rsObject.isClickable() && rsObject.isOnScreen())
  .map(RSObject::hover)
  .count();

if (count > 0) {
  General.println("Hovered trees");
} else {
  General.println("Couldn't hover trees");
}

Notice that the data source is defined by a method with a parameter for int ids (the API conveniently has all the normal tree ids).

The purpose of this stream pipeline is to hover all the valid trees that are found within the distance of 5 to the player. That being said, the terminal operation count, returns all the trees that were hovered, cached as a long.

Finally, checking if the count is greater than zero, which validates if any trees were hovered. 

Now, let’s discuss the importance of short-circuit stream pipeline processing. You’re already familiar with short-circuit evaluation with the logical AND (&&) and logical OR (||) operators. One of the nice performance features of lazy evaluation (short-circuit evaluation), is to stop processing the stream pipeline as soon as the desired result is available. 

The following example demonstrates the method findFirst and anyMarch (short-circuit terminal operations) that will process the stream pipeline and terminate as soon as the first object (player with dragon axe equipped) from the stream’s intermediate operation(s) is discovered. 

Predicate<RSPlayer> playerHasDragonAxeEquipped = (
                rsPlayer -> {
                    RSPlayerDefinition playerDefinition = rsPlayer.getDefinition();
                    if (playerDefinition == null)
                        return false;
                    return Arrays.stream(playerDefinition.getEquipment())
                            .anyMatch(rsItem -> rsItem.getID() == 6739); // Dragon axe id
                }
        );


RSPlayer dragonAxePlayer = Players.getAllList(playerHasDragonAxeEquipped)
  .stream()
  .findFirst()
  .orElse(null);

if (dragonAxePlayer != null) {
   Keyboard.typeString(dragonAxePlayer.getName() + " has a dragon axe equipped!");
   Keyboard.pressEnter();
}

// A cleaner way of doing this

// JustJ: In general you should think of it as a code smell.
// if you're doing orElse(null) which isn't an assignment to a object variable (for storage).
// In this case, use .ifPresent


Players.getAllList(playerHasDragonAxeEquipped)
    .stream()
    .findFirst()
    .ifPresent(dragonAxePlayer -> {
   		Keyboard.typeString(dragonAxePlayer.getName() + " has a dragon axe equipped!");
   		Keyboard.pressEnter();
	});

The predicate playerHasDragonAxeEquipped is applied to the first RSPlayer object in the stream. So, the predicate returns true if the player has a dragon axe equipped and processing of the stream terminates immediately.

Method findFirst() returns an Optional<RSPlayer> containing the object that was found. The call to Optional method orElse(null) returns the matching RSPlayer object or null, if any is not found. Even if the stream contained millions of RSPlayer objects, the filter operation would perform only until a match was picked up. 

Lastly, if the RSPlayer is found the script will type the player’s name and that they have a dragon axe equipped, then will press enter on the keyboard.

svFwRsdNMW0iRX6ta32DvBeThkpBowvBOMI7v6j2vsex_NfJjyixirkAzaivRGIHdbNQ6TtZ9oYscCpUmVulHK3iEUE-4NZ7wXf_T_LYwj6iSVep0pNVmaEJo1d2skzhsnsKD6I-

Wrap Up

In this tutorial, you worked with lambdas, streams, and functional interfaces. I presented many examples, showing simpler ways to implement tasks. You learned how to process elements in a Stream then used intermediate and terminal stream operations to create and process a stream pipeline that produced a result. You used lambdas to create anonymous methods that implemented functional interfaces and passed these lambdas to methods in stream pipelines to create the processing steps for the stream’s elements. We discussed how a stream’s intermediate processing steps are applied to each element before moving onto the next method call. I showed you how to use a forEach() terminal operation to perform an operation on each stream element using a method reference. You used intermediate operations to filter elements that satisfied a predicate and map elements to new values. You also learned some Tribot API, finding that the deprecated classes such as Filter and Condition were replaced by lambda’s Predicate and BooleanSupplier.

All in all, the way you think about Java Programming has been changed. Your code will become concise, and easier to debug. I encourage you to keep learning about lambdas and streams. There are more important intermediate and terminal operations, for example, reduce() and collect() . 

Thank you for reading my tutorial,

Polymorphic. 

 

Citations:

Deitel, H. M., & Deitel, P. J. (2010). Java: how to program. Prentice Hall.

Sources:

https://docs.oracle.com/javase/8/docs/api/

http://www.docs.tribot.org/tribot/latest/

Edited by Polymorphic
fixed part 3 2/2
Link to comment
Share on other sites

Looks really good! Always good to get more people on the stream chain :)


Just a few nits on the code examples:
 

int totalGold =
       Arrays .stream(Inventory.getAll())
               .filter(rsItem -> rsItem.getDefinition().getValue() > 4000)
               .mapToInt(value -> value.getDefinition().getValue())
               .sum();

-------------------------
// Do the map first, then we don't have to handle it twice
int totalGold = Arrays.stream(Inventory.getAll())
    .mapToInt(value -> value.getDefinition().getValue())
    .filter(value -> value > 4000)
    .sum();



 

int totalGold =
       Arrays.stream(Inventory.getAll())
               .filter(rsItem -> {
                   int gold = rsItem.getDefinition().getValue();
                   General.println("filter: " + gold);
                   return gold > 4000;
               })
               .mapToInt(value -> {
                   int gold = value.getDefinition().getValue();
                   General.println("mapToInt: " + gold);
                   return gold;
               })
               .sum();
--------------------------------------
// There's a great function called .peek which can be used here to make this cleaner
// I'm aware you're doing this as an example, but .peek is great for checking the state between steps

int totalGold = Arrays.stream(Inventory.getAll())
    .mapToInt(rsItem -> rsItem.getDefinition().getValue())
    .peek(value -> General.println("Gold: " + value))
    .filter(gold -> gold > 4000)
    .sum();


 

RSPlayer dragonAxePlayer =
       Players.getAllList(playerHasDragonAxeEquipped)
       .stream()
       .findFirst()
       .orElse(null);

if (dragonAxePlayer != null) {
   Keyboard.typeString(dragonAxePlayer.getName() + " has a dragon axe equipped!");
   Keyboard.pressEnter();
}

--------------------------------------

// In general you should think of it as a code smell if you're doing .orElse(null) which isn't an assignment to a object variable (for storage). In this case, use .ifPresent


Players.getAllList(playerHasDragonAxeEquipped)
    .stream()
    .findFirst()
    .ifPresent(dragonAxePlayer -> {
   		Keyboard.typeString(dragonAxePlayer.getName() + " has a dragon axe equipped!");
   		Keyboard.pressEnter();
	});


And just as a general Nit, I think it's probably not ideal to assume `.getDefinition` will not be null in all your examples and people will likely (mentally if not literally) copy/paste these examples, so it's likely better to either choose other examples if you want to keep them simple or make them null safe

Link to comment
Share on other sites

16 hours ago, JustJ said:

Looks really good! Always good to get more people on the stream chain :)


Just a few nits on the code examples:
 


int totalGold =
       Arrays .stream(Inventory.getAll())
               .filter(rsItem -> rsItem.getDefinition().getValue() > 4000)
               .mapToInt(value -> value.getDefinition().getValue())
               .sum();

-------------------------
// Do the map first, then we don't have to handle it twice
int totalGold = Arrays.stream(Inventory.getAll())
    .mapToInt(value -> value.getDefinition().getValue())
    .filter(value -> value > 4000)
    .sum();



 


int totalGold =
       Arrays.stream(Inventory.getAll())
               .filter(rsItem -> {
                   int gold = rsItem.getDefinition().getValue();
                   General.println("filter: " + gold);
                   return gold > 4000;
               })
               .mapToInt(value -> {
                   int gold = value.getDefinition().getValue();
                   General.println("mapToInt: " + gold);
                   return gold;
               })
               .sum();
--------------------------------------
// There's a great function called .peek which can be used here to make this cleaner
// I'm aware you're doing this as an example, but .peek is great for checking the state between steps

int totalGold = Arrays.stream(Inventory.getAll())
    .mapToInt(rsItem -> rsItem.getDefinition().getValue())
    .peek(value -> General.println("Gold: " + value))
    .filter(gold -> gold > 4000)
    .sum();


 


RSPlayer dragonAxePlayer =
       Players.getAllList(playerHasDragonAxeEquipped)
       .stream()
       .findFirst()
       .orElse(null);

if (dragonAxePlayer != null) {
   Keyboard.typeString(dragonAxePlayer.getName() + " has a dragon axe equipped!");
   Keyboard.pressEnter();
}

--------------------------------------

// In general you should think of it as a code smell if you're doing .orElse(null) which isn't an assignment to a object variable (for storage). In this case, use .ifPresent


Players.getAllList(playerHasDragonAxeEquipped)
    .stream()
    .findFirst()
    .ifPresent(dragonAxePlayer -> {
   		Keyboard.typeString(dragonAxePlayer.getName() + " has a dragon axe equipped!");
   		Keyboard.pressEnter();
	});


And just as a general Nit, I think it's probably not ideal to assume `.getDefinition` will not be null in all your examples and people will likely (mentally if not literally) copy/paste these examples, so it's likely better to either choose other examples if you want to keep them simple or make them null safe

Thank you for the feedback! All adjustments were made (let me know if incorrect again)

Link to comment
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

×
×
  • Create New...