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