Jump to content

Basic streams, and how to use them


Bobrocket

Recommended Posts

Here I will be covering streams. Streams are great ways to essentially sort through lists.

 

Why should we use streams?

Since you can do just about anything with them in a single line of code.

There are cases where streams probably aren't the most useful, take this:

NPC guard = getNpcs().closest("Guard");

This is our bog standard way of grabbing the closest "Guard" npc. What it would actually be doing behind the scenes would be using a stream like so:

public NPC closest(String name) {
    return getNpcs().getAll().stream().filter(npc -> (npc.exists() && npc.getName().equalsIgnoreCase(name))).min(new Comparator<NPC>() {
        @Override
        public int compare(NPC one, NPC two) {
            return Integer.compare(getMap().distance(one), getMap().distance(two));
        }
    }).orElse(null);
}

Looks complicated, right? Here's what it would look like if it wasn't all one line:

public NPC closest(String name) {
	Stream<NPC> unfilteredStream = getNpcs().getAll().stream();
	Stream<NPC> filteredStream = unfilteredStream.filter(npc -> (npc.exists() && npc.getName().equalsIgnoreCase(name)));
	
	Optional<NPC> npc = filteredStream.min(new Comparator<NPC>() {
		@Override
        public int compare(NPC one, NPC two) {
            return Integer.compare(getMap().distance(one), getMap().distance(two));
        }
	});
	
	return npc.orElse(null);
}

Here's a breakdown of all the functions: (more on comparators in a bit)

  • filter() -> This function will filter out our list for the specified properties that we're looking for in an NPC. In our case, we're looking for an NPC that exists and has a name of our variable "name". Every NPC that is in our region that doesn't suit our needs is discarded.
  • min() -> This is a bit of a complicated function, but what it will do is find the NPC which returns the smallest compare() value. In this scenario, it will return the NPC which is closest to us.
  • orElse() -> This is the same as an if (x) y; else z; statement, however basically what we are saying is if we could not find a suitable NPC, we can return the value inside orElse() (in this case, null).

Now, this probably still looks like a bunch of wizardry to you. No worries, streams are scary at first, but after you know how to write them they're great!

 

How to write a stream

By now, you're probably itching to write a stream. In this example, we're going to find all the players within a 6 tile radius.

To start off, we're going to have a function called getPlayersWithinSix():

public List<Player> getPlayersWithinSix() {
    return null; //We'll change this soon
}

Now we're going to begin the filter!

To start off, we want to grab all the players, and put it in a Stream<Player>. This is what we will be filtering!

public List<Player> getPlayersWithinSix() {
    Stream<Player> ourStream = getPlayers().getAll().stream();
    return null; //We'll change this soon
}

Now that we have our stream, we can finally begin filtering! In this example, we only want to filter to see if the player is within 6 tiles of our own player. We're going to store the filter in another stream like so:

public List<Player> getPlayersWithinSix() {
    Stream<Player> ourStream = getPlayers().getAll().stream();
    Stream<Player> ourFilteredStream = ourStream.filter(player -> (player.exists() && getMap().distance(player) <= 6));
    return null; //We'll change this soon
}

In our filter, we're checking these conditions:

  • Does the player exist?
  • Is the distance of the player within 6 tiles?

Finally, we want to turn our Stream into a list so we can use it. We do this like so:

public List<Player> getPlayersWithinSix() {
    Stream<Player> ourStream = getPlayers().getAll().stream();
    Stream<Player> ourFilteredStream = ourStream.filter(player -> (player.exists() && getMap().distance(player) <= 6));
    return ourFilteredStream.collect(Collectors.toList());
}

See how we changed our return to ourFilteredStream.collect();? What that will do is put all of these players we found into a list, and return it.

Now, this doesn't look like our streams from before, so to put it all in one line it would look like this:

public List<Player> getPlayersWithinSix() {
	return getPlayers().getAll().stream().filter(player -> (player.exists() && getMap().distance(player) <= 6)).collect(Collectors.toList());
}

Comparators, what do they do?

A comparator is basically a way we compare two objects. Since the Comparator type is abstract (much like our Filter<T> class), we have to make our own compare() method. An example can be seen in our NPC closest function:

new Comparator<NPC>() {
    @Override
    public int compare(NPC one, NPC two) {
        return Integer.compare(getMap().distance(one), getMap().distance(two));
    }
}

What this Comparator does, in essence, is compare the distances of both NPCs (relative to the player). Since getMap().distance() returns an integer, we use Integer.compare to compare both of the distances.

You'll most likely be using a Comparator to check for distance.

 

More stream examples

Here I'll be writing a few more stream examples to show why they're so useful.

Find the furthest away NPC that has the name "Man":

getNpcs().getAll().stream().filter(npc -> (npc.exists() && npc.getName().equalsIgnoreCase("man"))).max(new Comparator<NPC>() {
	@Override
	public int compare(NPC one, NPC two) {
		return Integer.compare(getMap().distance(one), getMap().distance(two));
	}
}).orElse(null);

Get all the NPCs in a 10 tile radius that you can attack:

getNpcs().getAll().stream().filter(npc -> (npc.exists() && npc.hasAction("Attack") && getMap().distance(npc) <= 10)).collect(Collectors.toList());

Get all the fishing spots where you can cage for lobsters:

getNpcs().getAll().stream().filter(npc -> (npc.exists() && npc.hasAction("Cage") && npc.hasAction("Harpoon"))).collect(Collectors.toList());

If I missed anything, let me know smile.png

Edited by Bobrocket
  • Like 8
Link to comment
Share on other sites

Stream<Player> ourStream = getPlayers().getAll().stream();
Stream<Player> ourFilteredStream = ourStream.filter(player -> (player.exists();

Keep in mind that you can only consume a stream once.

 

2b1b8cd73131c36ef890a190b8f4112a.png

 

Nice tutorial though laugh.png

 

 

Shouldn't matter in the example you provided though right? Or does the problem lie in the fact that I create a new Stream<Player>?

  • Like 1
Link to comment
Share on other sites

Shouldn't matter in the example you provided though right? Or does the problem lie in the fact that I create a new Stream<Player>?

 

The example you provided is legal dw :)

 

This however wouldn't be:

       List<String> list = new ArrayList<>();
        Stream<String> stream1 = list.stream();
        Optional<String> optional1 = stream1.findAny();
        Optional<String> optional2 = stream1.findAny(); // <-- Illegal
        Stream<String> stream2 = stream1;
        Optional<String> optional3 = stream2.findAny(); // <-- Illegal

That's why it's recommended to keep both the creation and consumption of Streams in the same one line of code.

  • Like 1
Link to comment
Share on other sites

The example you provided is legal dw smile.png

 

This however wouldn't be:

       List<String> list = new ArrayList<>();
        Stream<String> stream1 = list.stream();
        Optional<String> optional1 = stream1.findAny();
        Optional<String> optional2 = stream1.findAny(); // <-- Illegal
        Stream<String> stream2 = stream1;
        Optional<String> optional3 = stream2.findAny(); // <-- Illegal

That's why it's recommended to keep both the creation and consumption of Streams in the same one line of code.

 

I had it laid out like that so people can see each individual step, I'll add that in a bit though :)

 

  • Like 1
Link to comment
Share on other sites

Stream<Player> ourStream = getPlayers().getAll().stream();Stream<Player> ourFilteredStream = ourStream.filter(player -> (player.exists();
Keep in mind that you can only consume a stream once.

2b1b8cd73131c36ef890a190b8f4112a.png

Nice tutorial though laugh.png

He hasn't consumed the stream yet, he's just storing it in a variable.

@OP It's best to say streams are good for "collections" rather than lists, since it also applies to sets and maps (although a map isn't a "true" collection, it can still be seen as one).

Other than that, this is a pretty nice guide :) Although in my opinion, the code used to demonstrate is a bit verbose (anon class and a bunch of conditions), which may be a distraction to some. I recommend finding a more pleasing chunk of code to work with for the sake of teaching.

map and flatMap are also extremely useful, I highly suggest checking them out

  • Like 1
Link to comment
Share on other sites

Two minor improvements:

  1. Create a separate function to handle the logic of the filtering. It makes the Lambda expression more readable.
  2. Include comparator as a Lambda expression also.

For example:

	/**
	 * 
	 * @param npc
	 *            NPC object instance
	 * @param name
	 *            Name to check against (case insensitive)
	 * @return <tt>NPC is defined</tt>
	 */
	private boolean isDefined(NPC npc, String name) {
		return npc != null && npc.exists() && npc.getName().equalsIgnoreCase(name);
	}
	
	/**
	 * 
	 * @param name
	 *            Name to check against (case insensitive)
	 * @return NPC object instances
	 */
	public NPC getClosestNPC(String name) {
		return getNpcs().getAll().stream()
				.filter(npc -> isDefined(npc, name))
				.min((npc1, npc2) -> Integer.compare(getMap().distance(npc1), getMap().distance(npc2)))
				.orElse(null);
	}

Oh and everything after #stream(), I break-line each of the running expressions, to make them more readable also.

  • Like 2
Link to comment
Share on other sites

Two minor improvements:

  • Create a separate function to handle the logic of the filtering. It makes the Lambda expression more readable.
  • Include comparator as a Lambda expression also.
For example:
/**	 * 	 * @param npc	 *            NPC object instance	 * @param name	 *            Name to check against (case insensitive)	 * @return <tt>NPC is defined</tt>	 */	private boolean isDefined(NPC npc, String name) {		return npc != null && npc.exists() && npc.getName().equalsIgnoreCase(name);	}		/**	 * 	 * @param name	 *            Name to check against (case insensitive)	 * @return NPC object instances	 */	public NPC getClosestNPC(String name) {		return getNpcs().getAll().stream()				.filter(npc -> isDefined(npc, name))				.min((npc1, npc2) -> Integer.compare(getMap().distance(npc1), getMap().distance(npc2)))				.orElse(null);	}
Oh and everything after #stream(), I break-line each of the running expressions, to make them more readable also.
Line breaking while method chaining is also an easier way to find the source of an exception when one is thrown
Link to comment
Share on other sites

Two minor improvements:

  1. Create a separate function to handle the logic of the filtering. It makes the Lambda expression more readable.
  2. Include comparator as a Lambda expression also.

For example:

	/**
	 * 
	 * @param npc
	 *            NPC object instance
	 * @param name
	 *            Name to check against (case insensitive)
	 * @return <tt>NPC is defined</tt>
	 */
	private boolean isDefined(NPC npc, String name) {
		return npc != null && npc.exists() && npc.getName().equalsIgnoreCase(name);
	}
	
	/**
	 * 
	 * @param name
	 *            Name to check against (case insensitive)
	 * @return NPC object instances
	 */
	public NPC getClosestNPC(String name) {
		return getNpcs().getAll().stream()
				.filter(npc -> isDefined(npc, name))
				.min((npc1, npc2) -> Integer.compare(getMap().distance(npc1), getMap().distance(npc2)))
				.orElse(null);
	}

Oh and everything after #stream(), I break-line each of the running expressions, to make them more readable also.

 

Thanks for this. I'm still getting my head around lambdas and I didn't realise you could have comparators as lambdas, I'll add that in the thread at some point :)

Link to comment
Share on other sites

Thanks for this. I'm still getting my head around lambdas and I didn't realise you could have comparators as lambdas, I'll add that in the thread at some point smile.png

 

There can be as many arguments as you like, however the expressions can easily become cluttered and messy, so if you've got a lot of parameters, consider boxing them inside of an new object and passing that object as a single parameter. When you have multiple arguments, you have to wrap them in brackets and give each parameter a name, e.g.;

#forEach((param1, param2, param3, param4, param5, param6, param7) -> doSomething(param5));

This is why Comparator works and why I've used it as so in my earlier post.

 

Any interface can be used just so long as that interface is a functional interface, aka. an interface that only has one method. For instance, in Lambda, you cannot use MouseListener (or the other mouse/key listener variants), because they have two or more required methods implementations. 

 

If you wanted to implement interfaces via Lambda expressions for one MouseListener function (e.g., #mouseClicked(MouseEvent)), then you could do a work around such as this:

import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.util.ArrayList;
import java.util.List;

public class MouseClicked implements MouseListener {

	// Create a list to store our new functional interface type (below)
	private final List<Listener> listenerList;
	
	public MouseClicked() {
		this.listenerList = new ArrayList<>();
	}
	
	@Override
	public void mouseClicked(MouseEvent e) {
		
		// Check if list is empty
		if (!listenerList.isEmpty()) {
			
			// Iterate through the list and pass the MouseEvent to the functional interfaces
			listenerList.forEach(listener -> listener.mouseClicked(e));
		}
	}
	
	// Delegate methods:
	
	public void addListener(Listener listener) {
		listenerList.add(listener);
	}
	
	public void removeListener(Listener listener) {
		listenerList.remove(listener);
	}
	
	
	// New functional interface:
	
	public static interface Listener {
		
		void mouseClicked(MouseEvent e);
	}
	

	/*
	 * To hold true to the interface "contract", we will have to implement the
	 * other methods that come packaged with MouseListener. However since we
	 * don't need to use them/interact with them, we can leave their code bodies
	 * empty.
	 * 
	 * Irrelevant methods implements:
	 */

	@Override
	public void mousePressed(MouseEvent e) {
		// Ignored.
	}

	@Override
	public void mouseReleased(MouseEvent e) {
		// Ignored.
	}

	@Override
	public void mouseEntered(MouseEvent e) {
		// Ignored.
	}

	@Override
	public void mouseExited(MouseEvent e) {
		// Ignored.
	}

	
}

Then in your script:

	public void doSomething() {
		
		// Create new object, which also functions as valid MouseListener
		MouseClicked mc = new MouseClicked();
		
		// Add MouseClicked listeners as Lambda expressions
		mc.addListener((e) -> System.out.println(e));
		
		// Register the entire listener
		getBot().addMouseListener(mc);
	} 
Edited by liverare
  • Like 1
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.
Note: Your post will require moderator approval before it will be visible.

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.

  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...