Java 8: A Modern Solution for Modern Challenges
Java's Origins as an Object-Oriented Language:
A Good Run
From Java applets to desktop GUIs, Java has been used in many ways, but most people are familiar with Java for its use on the backend in N-tier web applications. Throughout the 1990s and 2000s, OOP dominated the programming landscape. Business use cases were modeled as objects, with classes designed in hierarchies as chains of inheritance, and complex object graphs represented through object composition.
An entire discipline, known as the Gang of Four (GoF) design patterns was preached and practiced as the most powerful approach to model a business use case as an OO solution. In Java, these objects representing business entities were popularly known as Plain Old Java Objects (POJOs).
POJOs were powerful in another way. They reflected what was then the ubiquitous way of representing records in relational (RDBMS) databases. A single POJO, or entity, represented one record in a database table. A collection of POJOs represented a query of records from a database table. An object graph in Java was mirrored by the joins between tables. New frameworks to simplify the persistence of objects in an RDBMS database, called object-relational mappers became common place. Hibernate, the most popular of these, has been used in many n-tier applications.
A Changing Landscape Threatens Java's Reign
In the late 2000s and early 2010s, the software development landscape began to undergo significant structural changes. Cloud-hosted solutions replaced the server room, and data analytics emerged as a response to the arrival of Big Data and the need to understand it all.
The CPU had reached its limits, and distributed computing rose to meet this growing demand for computing power. Software applications ran across clusters of server instances, and distributed database solutions arrived to meet the demand for storing and querying Big Data in a way that RDBMS simply no longer could.
New architectures for these emerging technologies made object-oriented programming obsolete, and POJOs persisted in discrete transactions weren't as effective anymore. The data streaming architecture that we know today uses the log representation of data, where data is stored as events in an event log.
Another architecture that has increased in popularity as a replacement for the POJO model, is CQRS, or Command Query Responsibility Segregation. Rather than updating single row records in a RDBMS database, the changes to those entities are stored as events in an event log, and persisted to NoSQL databases, such as Cassandra or DynamoDB.
Functional languages that had emerged in the early 2000s seemed a natural fit for working with these streams of data. Rather than manipulating POJOs with procedural logic, applying the business logic by mapping functions over the data as it flowed was found to be the more elegant solution.
Where Java classes have "methods," functional languages exalted these methods by declaring that functions were themselves objects. In fact, data streaming and concurrency frameworks, such as Kafka, Spark and Akka, are written in Scala, which is arguably the most popular functional language on the market today.
Under Sun, the original owner of the Java language, Java releases didn't progress according to a strategic vision. New versions were released after it was deemed that enough features and bug fixes had accumulated to justify a release.
From versions 1 through 7, Java remained fundamentally the same -- an object-oriented language designed for the development of n-tier web services. Facing the swing of the pendulum from object-oriented to functional, Java set about re-imagining itself as a hybrid of both object-oriented and functional features.
In March 2014, Java 8 was released. With the incorporation of several features of functional programming, Java 8 represented the largest transformation between any prior releases. The functional features of Java largely mirror those found in a Scala, Java's functional JVM sister language.
Java Responds
We helped our clients create their impact in the software industry? Check out our case studies today!
1
Optionals
Traditional imperative Java programming presents several scenarios where a request for data returns nothing. An uninitiated class field, the first item of an empty list, or no database record for a given query all result in null.
Handling situations that can produce null values requires defensive programming with if-else logic to check for empty lists and uninitiated fields. Failure to catch null situations result in the dreaded NullPointerException, which seldom results in graceful error handling.
Optionals are a functional programming construct borrowed from the Scala Option. Optional is a context in which values or objects are contained. If there is data, then the data is contained in that Optional.
In the above null scenario, the result is an Optional.empty. The values within Optionals can be manipulated by applying functions through Option.map(). The benefit of this, is that Optional.empty handles null scenarios in a more deterministic way without expensive exception handling.
2
Lambdas
Pre-Java 8, logic was contained in class members known as methods, which are functions that essentially belong to the class in which they are defined.
Lambdas are simply functions that have been elevated to object status. They can exist independently of the classes in which they are defined, and as objects, can be passed to other functions and methods as parameters.
They fit well within the functional programming space and are arguably functional programming’s most defining feature. Not only can lambdas can be mapped over Optionals to transform the values contained therein, they can also be mapped over Java collections and streams.
Prior to lambdas, working with collections required verbose imperative coding to manipulate the collection mechanically. Mapping lambdas over collections results in a more algebraic style of reasoning and more concise code.
3
Streams
Streams, which should not be confused with input and output streams of the original Java IO library, are a powerful new functional feature of Java 8. Java 8 streams are a functional representation of collections and effectively an abstraction of data sources.
While streams can be created from finite Java collections, they can also be attached to a data source that continually produces or generates data. This provides an ability to transform, enhance, and filter data elements from an effectively infinite source with the same functional programming approach used on finite collections.
Coding is more concise when a continuous stream of data can be reasoned about as a distinct set of data.
Looking for more than Java? Check out what we can offer you!
Too Much of a Good Thing: Drawbacks of Implementing the Functional Features in Java 8
As previously mentioned, Java was originally designed as an object-oriented language, and to many Java developers, remains fundamentally that way. N-tier web services backed by relational databases are still very much with us and representing business entities with POJOs is a regular practice. While early adopters of Java 8's functional features have torn into this new paradigm with passion, a large wellspring of talent still exists in the object-oriented space.
Functional programming represents a more algebraic way of thinking, while imperative programming is more mechanical and logical. Whether learning a purely functional language from scratch or learning how to use the new Java 8 features, many consider functional programming to present a steep learning curve to the novice developer. Recruiting talent in a tight labor market is a serious concern for many businesses staffing a project.
Adoption of functional or "fancy" new Java 8 features should also take the complexity and side-effects of those features into consideration. Eager and pervasive use of lambdas by early adopters can have adverse consequences, and inconsistent use of lambdas affects the readability of code.
In addition, Java's verbosity should be viewed as a good thing. Sometimes, by intentionally writing extra code, you will produce code that's easier to understand and easier to maintain as a result. Functions are great tools, but they should be used responsibly and not just used to decrease the number of characters in code. Functions should be used to create code that is easy to understand. This will help you produce high performing systems that are easier to maintain in the long run.
Furthermore, when it comes to any language, new features are often neither stable, nor highly performant. You should determine if the Java version you're using is stable, and if that version will be supported over the next two to three years. When in doubt, POCs and spikes are a good way to test new features for stability and performance.
Use What You Need
A good approach for deciding whether to favor an object-oriented approach or a functional one is to base your decision on the technology stack for the project. Here are some perspectives and use cases to consider:
Optionals Solve an Age-Old Problem
Optionals are the easiest functional feature to work with, and they're a good starting point for those new to the functional paradigm. Not only do they solve the age-old problem of the NullPointerException, but they also make for better modeling of the use cases that result in nulls.
For example, an Enrollment entity with an empty list of students is not an error case that should throw an exception since it might just be a course that hasn't yet enrolled any students. It's a good idea to begin the move to functional programming by replacing verbose yet brittle defensive programming with a functional approach more representative of these use cases.
Frameworks Show the Way
Scalable, commercial solutions are seldom, if ever, written in raw Java. Open source frameworks and libraries are often used to abstract away the boilerplate Java code of REST interfaces, persisting data and generating the presentation layer.
These frameworks often require lambdas in their interfaces and method signatures. Since these frameworks themselves are built using recognizable patterns, conforming to their interfaces results in code that is more likely to conform to patterns and best practices.
For example, the Spring framework uses lambdas for CRUD logic in its persistence templates. Akka, a popular concurrency framework, uses lambdas for the receive function in its actors, the core unit of Akka design.
Non-N-Tier Applications and Java 8 Streams
N-Tier applications are still required and found in abundance, but they have been partially displaced by new architectures in response to data analytics, distributed computing, and even streaming technologies.
POJOs persisted by CRUD operations are the mainstay of N-tier apps, and as a result, they rely more heavily on the original object-oriented design of the Java language.
New technologies, however, don't see the data as discrete RDMBS records, but rather as a continuous stream of data flowing through the application. These streams in the data modeling sense are more amenable to Java 8 streams, as mapping transforming and enhancing functions over the data is more practical in this case.
A typical data streaming example might be comprised of a Kafka cluster to enqueue arriving data, any combination of Spark Streaming and Akka Streams to enhance and transform the data, and a Cassandra database to capture the aggregate result.
In this case, Java streams and lambdas would be recommended over traditional object-oriented Java programming.
Let's get together. Set up your free technical consultation and find out how we'll bring our vision, mission, and values to successfully execute your next project.
From its earliest days as the platform-independent language for web applets and desktop apps, Java has remained relevant by growing to meet the needs of a changing technology world.
Concise languages with opinionated frameworks such as Ruby on Rails have arisen for rapid development of defined scope applications, and functional languages have grown in popularity due to their effectiveness in data analytics and working with event logs.
Java has faced these changes to the technology landscape by adapting. Java 8 still serves the classic n-tier web application through frameworks like Spring Boot and Hibernate, and it’s as effective as ever at delivering as an object-oriented language.
For newer technologies and approaches, like data analytics and CQRS event logs, Java 8 offers powerful functional features. By choosing a strategy with the language features and open source frameworks and libraries most suited to the project, Java 8 offers the power and flexibility to meet your needs.
Find out how our expert Java developers will add value to your project by scheduling a call with us!
Related Content
We built a custom recommendation engine from scratch using Java and several other technologies. Click the link to learn how we did it.
We overcame significant challenges to launch our Java legacy app rebuild project for our manufacturing client, and you can read more here.
Subscribe to our newsletter!
We've been in the software industry for 30+ years so we have a lot to share with you!
Follow US
Address: 16870 W Bernardo Dr, Suite 250
San Diego, CA 92127
Email: info@integrant.com
Phone: +1 858.731.8700
© 2021 Integrant, Inc. All Rights Reserved