Here in this blog I have explained about parallel functional programming, which focuses on the concepts, techniques, tools, and methods needed to develop Java programs whose computations run in parallel on modern multi-core processors. I've observed a growing demand for software developers who understand how to write parallel functional programs for a range of computing platforms, including mobile devices, laptops and desktops, and servers in cloud computing environments.
This demand is driven by advances in high-performance multi-core processors, however, leveraging these powerful processors effectively requires you to have a deep knowledge of parallel functional programming to increase productivity and performance. Therefore, the goal in this course, is to teach you how to develop high quality and scalable functional programs.
Throughout the blog, you'll master the structure, functionality and APIs of Java's parallel streams, fork-join and completable futures frameworks. In addition to learning about these frameworks, I present many case study applications that demonstrate how to apply them in practice. Our advancedblog also covers core Java functional programming features, such as lambda expressions, method references, and functional interfaces.
To get the most out of this blog, you should be familiar with common software patterns, as well as object-oriented design concepts, such as encapsulation, abstraction, extensibility, and polymorphism, and core Java object jointed language features, such as classes, inheritance, dynamic binding, and generics. The topic of parallel functional programming in Java is much broader and deeper.
Now that you've learned about the history of Java, and its concurrency and parallelism mechanisms and frameworks, I am going to mention here about when to apply these different mechanisms, and under what circumstances they should be used. Java's concurrency and parallels and mechanisms span many layers in modern software stacks.
These would include things like the Java development services platform, the Android platform, and so on. And deciding which of the mechanisms to pick depends on various factors that you have to be aware of. If you're developing low-level classes or performance-sensitive apps, such as gaming or mobile gaming, you might want to consider using the shared object mechanisms we talked about. These are things you would find in the java.util.concurrent package. Things like Reentrant Lock, conditioned object, Semaphore, the Atomics and so on. And the reason you might want to select these is because they're designed to be very efficient and very lightweight.
So, there's not a lot of overhead, they're super-duper optimized, there's lots of clever algorithms, there's lots of clever use of native interfaces to speed things up. Lots of clever use of the underlying hardware mechanisms like comparing swap operations and so on. The downside of course, is it's relatively tedious and error-prone to program at this level of abstraction. And therefore, shared objects are often best used by infrastructure developers versus end-user app developers.
Unless your end user-app, as I mentioned before, is highly performance-sensitive like a gaming app. If you're developing frameworks, things that reside on top of these lower-levelmechanisms, then you might want to consider using Java's message-passing mechanisms. And these include various blocking queues like, a ray blocking queue or linked blocking queue. So, for instance, if you were developing the frameworks, you would find in say Android, like the Android a sync task framework or the handler’s messages and runnable or hammer frameworks, that's a good place for using Java's message passing mechanisms.
Likewise, if you take a look at Java fives, frameworks in the executor framework, like the executive service, the executor completion service, they use a lot of those underlying message-passing mechanisms to do the various decoupling and interactions between producer and consumer threads. The benefit of using message-passing relative to shared objects, is they tend to be more flexible and they tend to be more decoupled. Decoupled because you separate the threads that are producing work from the threads that are consuming the work through the use of a thread safe queue.
The downside is that by using this message-passing model, you can end up with a bit more time and space overhead because there's typically more context switching, more synchronization more data movement or memory management required in order to be able to do message-passing compared with shared object operations. If you're building apps, particularly things like mobile apps, which is all the rage these days, you might want to consider programming with the higher-level frameworks we discussed.
Things like Java parallel streams, things like completable futures, as well as other frameworks that are very popular as well, like RX Java, or some of the mechanisms that are provided in later versions of Java, like the reactive streams model in Java nine and beyond. The benefits of using these higher-level frameworks, is the improvement in programmer productivity, because you have to worry about a lot of less details, which are done by the frameworks rather than by you.
And because these frameworks have been developed and debugged and optimized over the years, they tend to be very robust. The downside of course, with using these frameworks is again, there could be some time and space overhead, and they can be overly prescriptive for situations that don't fall neatly into a divide and conquer or react to the synchronous pair of models, which are what parallel streams and completable future support respectively.
On the other hand, this really is a great way to go and not surprising, that's going to be the focus of most of what we'll cover in the rest of this course. A full stack developer should be able to understand the concepts and mechanisms that each of the layers involved in a modern platform ranging from the operating system kernel, to the system libraries, to the execution environments, the virtual machines, for example, the, if you're running an Android, the Dalvik VM, the Android runtime and so on. Up to the concurrency and parallel frameworks we have in the Java platform, the Java class library, as well as other frameworks that may be built on top that.
So, these tutorials we're giving here are going to focus on the higher levels. But be aware that if you're really trying to build yourself as a coveted, full stack developer, it's important to understand the whole span from top to bottom.
Now that we've covered the concurrency and parallelism concepts, and frameworks, and mechanisms provided in Java, especially over time. It's time to talk about how we're going to be applying Java's functional programming features and frameworks in several example case study apps throughout the rest of this course. All of these apps are available in open source form from my GitHub repository.
So please go ahead and clone that repository and you'll find lots and lots and lots of code, which corresponds not only to what we cover in this blog, but also other blogs I have in other contexts as well. So,I am going to mention briefly about the different case study apps we'll be covering in this lesson throughout the course. One of the first case study apps we'll be covering is something called the Thread Join Test, which is a sample app that shows how Java is functional programming features like lambda expressions, and method references are applied to search in parallel for a list of phrases in the complete works of William Shakespeare.
And the above example is neat because it demonstrates how simply knowledge of Java's functional programming mechanisms can be coupled with Java threading in order to get an improvement over more traditional object-oriented threading mechanisms. However, as we'll also see there's a lot to be desired by using threads on their own. And therefore, the rest of the course will cover other case studies that give us more interesting functional, programming and especially parallel functional programming capabilities to illustrate these features and frameworks in Java.
There's another case study called the Simple Search Stream case study that we'll use to show how Java's sequential streams are applied to find words in the lyrics of songs. And this example is really just kind of a warm-up to get you familiar with the capabilities provided in the Java streams framework. And we'll also provide a similar but enhanced case study called the Search Stream Gang case study, which will initially look at a sequential stream as approach just to give you a concept of how things work, and then later we'll also be generalized to support parallel streams.
And what this case study is really a family of case studies allows you to learn about or how to apply Java regular expressions to efficiently match phrases in the complete works of William Shakespeare. And this is a really cool example and it also gives us a chance to compare and contrast a whole bunch of different Java sequential and parallel processing frameworks to see how they scale up in terms of runtime performance on a modern multi core processor, in this case either a quad core, six core multiprocessor to show how different models scale in different ways.
There's also another case study that will apply called the Image Case Study. And this app will demonstrate how you can use completable futures and the completable futures framework to recursively crawl web pages and count the number of images in parallel, using Java's asynchronous reactive programming model. And what's cool about this is this example lets you both crawl things local on your computer, as well as crawl things remotely in the web using identical programming interfaces.
So, it's a really cool example and I think you'll like it quite a bit. And it's actually something you could probably use in your own programs without a whole lot of change, just changing what you're doing as you visit various nodes that you find as you do a recursive descent through a nested hierarchy of folders. And then we're also going to cover a case study app called the Image Stream Gang app. And this is going to show how various Java frameworks in particular parallel streams that completable futures can be applied to download, filter, store and display images in parallel, that are downloaded from web servers across the network.
And this is also really cool, because it'll give us a chance to do apples to apples comparisons between these different parallel functional programming frameworks that Java supports. And you'll see as always, there's fascinating tradeoffs between programmer productivity on the one hand, and the ability of the system to perform and take better advantage of course. And so I think you'll really get a good sense of the tradeoffs between the different frameworks that Java supports.
we're now going to talk about other properties, of functional interfaces above and beyond the ones that we've talked about so far. Functional interface, of course, need to have one abstract method, as in the case of the other five examples we've looked at so far so, test for predicate, apply for function and by function, get for the supplier and except for consumer, but it's actually also possible for functional interfaces to have more than one just method.
So, functional interfaces can have default methods, and they can also have static methods, so let's take a look at a concrete example, this is the comparator interface we talked about briefly earlier, when we were talking about sorting list and the comparator interface is basically a way of imposing total ordering on some collection of objects, so here is the abstract method that is defined in the comparator it's called Compare, that's not big surprise, you take two objects of type T, which passes as the parameter to comparator, and then returns zero if there're same, returns minus number if the first one is less than the second, and it returns a positive number.
See here there's method called Reversed, and this goes ahead and will do a comparison, but it will do it in reversal order, so, rather than having the first value, and the second value, being the order that you call to compare, It will swap it around, you can actually override this method if you wanted to make your own implementation of comparator to reverse it in some other way.
It's also possible to have static methods, in functional interfaces, so here we have reverse order, which is a static method, and because it's a static method, you can't override it, It's the one and only implementation of this method, for this interface, now there is yet another type of method here, which is really mysterious, and at first glance it looks wrong, and this is the equals method, and the equals method of course, is going to be used to compare whether, an instance of the comparator is equal to another object which is also a comparator.
Now what makes it interesting is equals is also an abstract method, well how could that be? You might ask, I thought that functional interfaces could only have, one abstract method? Well it turns out there is an exception, or an escape clause in functional interfaces, and any methods that are defined in, the java.lang.objectclass, that are going to be overridden in a functional interface, don't actually count, as part of that interfaces abstract method count.
So, methods like equals, hash and so on, there're various methods that can be overwritten from java.lang.object, you can have those in a functional interface, without having it count or as the one method that's abstract in a functional interface, and yes I agree that, that's very strange but I think that, that's really a characteristic of the fact that when Java was first designed back in the day, they didn't have all the modern notations of making interfaces cooler and more powerful.
So, they might have done something different if they had concept of functional interfaces from day one, and they might have defined jave.lang.object, to be an interface with default methods, as opposed to being this special class that is treated in somewhat exceptional way, by the Java compiler and runtime system.
After I mentioned details about the Function functional interface, I am now going to explain about a slight generalization to Function called BiFunction. And as before, we'll show an example of this in the context of the Java collections framework. So, a BiFunction is a functional interface that applies a computation on two parameters and returns a result.
So unlike Function, which only worked on one parameter, the apply method on BiFunction takes a T and a U, which are two different types, or could be the same type, but it could be different types, and returns a value of type R. So,I am going to go back to our map example using our Stooge map which maps the Stooges to their IQs, so it's mapping stringed integer. As before, we initialized the map with Larry having the IQ of 100, Curly having the IQ of 90 and Moe having the IQ of 110.
And then we're going to show a couple of different ways to manipulate the map entries. So, the first way, kind of a conventional way of doing this, is to use a forEach loop, which is going to iterate through every entry in the entry set of the map. And what we're going to do is we're going to take the current value, which is the IQ of a Stooge, subtract 50 points from it because perhaps the Stooges have gotten even dumber than they used to be, and then we're going to go ahead and set the value with the updated value. So that's kind of the classic way of doing things.
An alternative way of doing things is to use a BiFunction lambda, which in this case is going to subtract 50 IQ points from each Stooge in the map using the replaceAll() method. And I think you'll agree just by looking at this it's a lot more concise. What replaceAll is going to do is it's going to do the same thing that that loop did, except it's going to centralize it in one method and that's going to have a couple of other benefits.
So, one of the benefits for using replaceAll instead of using the forEach loop is that replaceAll operates in a thread-safe manner, whereas the earlier approach does not operate in a thread-safe manner at all. So, if multiple threads were setting and getting the values in the map in that manner, you'd have chaos and insanity as the result, whereas replaceAll will be much cleverer about how that works.
Let's take a look at replaceAll. I believe the other reason why you'd want to use replaceAll is it's just more concise. It does it all in one fell swoop. So, you can see replaceAll is passed in a BiFunction, which oddly enough in the implementation is called Function. Probably should've called it BiFunction, but the parameter name is Function and it's a BiFunction. The particular example here, we're going to be passing in the lambda expression v - 50. So that's value minus 50, where K is the key and V is the value. Here's what this looks like when we get into the actual implementation. When we're doing the replaceAll, as you can see, we're iterating through all the entries in the map.
Actual implementation:
Here, the apply() method here is replaced by v-50 BiFunction lambda.
And for each of the key value pairs in the map, we're going to apply the function that's passed in as a parameter and we're going to give it the key and the old value, and it's going to go ahead and compute a new value and it's going to replace the old value with the new value in the map. So that just illustrates a nice way of being able to apply BiFunction. Not really all that much different, quite frankly, from Function. Just allows you to have two parameters, in this case the key and the old value, as opposed to just having one.
We hope you found this article useful. Here at Cloud Employee, we pride ourselves on being a supportive and cutting edge workplace continuously investing in staff development, engagement and well-being check out our Careers Page to watch videos, find out more and submit your CV if your would like to join our community