The high level goal of this course is to learn how to transition from coding for courses to coding in the real world.
A lot! Teaching is my hobby; writing code is what I actually do for a living. I am teaching you the things I wish I had known when I graduated.
“Candidates used to struggle to get through the technical portion of interviews…now the technical questions are largely useless, because students do so well now…”
“…we started probing the students to find out what had changed and the answer was CSC-430”
A local employer (paraphrased)
Component | Weight |
---|---|
Assignments | 68% |
Exams | 28% |
Misc | 4% |
For this course, we will be using the following textbooks, in addition to free online resources:
You are expected to read everything that is assigned. If you don’t:
Don’t be afraid to ask questions!
I am available during my office hours and online throughout the day via email and the #csc430 slack channel.
Additional office hours can be planned in advance, so contact me!
Don’t be afraid to ask questions!
Don’t be afraid to ask questions!
Canvas will be used to handle the general organization of the course and all critical announcements.
Other reminders, notes, etc. may be distributed via twitter @msupwright4 and slack.
My goal is to push you hard. Easy classes are not worth the money you are paying and a degree with no actual skills is worthless.
Be responsible. Ask questions. Do your work.
To understand why this class exists, we need to first analyze how you code now, and then we can talk about why this does not scale beyond the classroom.
Most of your coding experience probably consists of projects with less than 100 lines of code.
How do you manage this code?
Some line counts for a few projects I work on:
At these sizes, just working with the code (distributing, sharing, etc.) becomes a non trivial task.
We need a way to backup our code, track changes, and avoid conflicts with coworkers–at scale.
How do you test your course projects?
Manually testing large codebases is, literally, not possible without doing a poor job.
The number of paths in your code to test grows exponentially! If the time you spend testing does not, then you are not testing your code.
Even writing tests for large codebases is not possible if the code is written poorly.
Accordingly, we need to automate testing and write our code in a way that makes it feasible to write sufficient tests.
How do you build your code?
Do you even know how you build your code?
As projects grow in size and complexity, even compiling, building, and deploying your code becomes a problem.
We can not rely on manual steps!
Instead, we must automate the build process (including testing!) to ensure that we can deliver code in a reproducible, safe way.
Ideally, we automate deployment as well.
How hard is it to maintain your code after a year?
You don’t know, because you throw it away after a week!
In the real world, your code will live for years (or decades) and will have to be maintainable by the unlucky individual that gets stuck with your legacy code.
Often, you are that unlucky individual.
Also often, you will not even understand your code if you are not careful with how you write it.
We can largely conquer these problems (and more) by simply caring about our code and automating all the things.
Humans suck at coding and we must humbly accept all of the help we can get from tools, processes, etc.
According to its own website…
Apache Maven is a software project management and comprehension tool […] can manage a project’s build, reporting and documentation from a central piece of information
We will boil that down to the following, though:
Maven is a dependency management and build tool.
What is dependency management?
Code you work on for your courses is often completely self contained, in one or two class files.
You will typically only be importing other classes from the standard library.
In a real world project, though, you will typically be relying on a significant amount of code written by others.
This code will be packaged in jar files which you will need to have available when building and distributing your code.
In the bad old days, this meant:
This may not sound to bad, but on large scale projects, this can be a huge source of problems!
A dependency manager will allow you to provide a small amount of configuration, and it will then handle all of these problems for you in an automated manner.
When we talk about a build tool, we are generically referring to any tool that allows you to provide a configuration (or script), which can then handle all build steps that are necessary to produce your end product.
For instance, you could use a build tool to trigger dependency management, compile your code, execute automated tests, package your compiled code, and more!
A keep theme here is automation.
If our build process is too complicated, we will forget steps and make mistakes.
This will lead to inconsistencies and errors.
Complex manual processes also make it difficult to work with collaborators, because it takes significant work just to get the code running the same on all developer machines.
Instead, we use a clear, precise configuration and feed it to a build tool to guarantee that we have reproducibility anywhere our code is built.
This also allows us to reduce our build process to a single command!
There are usually multiple build tools that can be used for any given programming language, but Maven is one of the most commonly used in the Java world.
You may be interested in becoming familiar with Gradle as well, though.
Maven relies on a configuration called a Project Object Model (POM) file.
Our main concern at this point is how to configure dependencies.
For simple projects, the building works out of the box!
To add a dependency, we simply need to provide the group id, artifact id and version of the library you want to use.
We call these the coordinates of the artifact.
For example, we might add a dependency on a course library like:
<dependencies>
<dependency>
<groupId>edu.murraystate</groupId>
<artifactId>BlobAPI</artifactId>
<version>1.0</artifactId>
</dependency>
</dependencies>
Note that, when you add a dependency, it may also need its own dependencies.
Fortunately, Maven artifacts are packaged with their own POM file, so Maven will go ahead and download all dependencies transitively.
Maven is configured, by default, to pull artifacts from Maven Central, which is a public, centralized artifact repository.
You may, however, need to use custom, private repositories.
<repositories>
<repository>
<id>BlobAPI-mvn-repo</id>
<url>https://raw.github.com/MSUCSIS/csc430-maven/mvn-repo/</url>
<snapshots>
<enabled>true</enabled>
<updatePolicy>always</updatePolicy>
</snapshots>
</repository>
</repositories>
Design patterns are, ultimately, nothing more than the result of applying a few object oriented principles which you should try to follow.
We will be learning those principles, but it is also good to study some of the patterns that arise from their use so that we don’t have to reinvent the wheel.
Learning established patterns allow you to quickly get to a solution, and they also provide you and other developers with a shared vocabulary that can be used to discuss your code.
During the semester, we will also be discussing how design patterns are actually kind of awful and may be seen as a “least awful” solution to problems in many cases.
The strategy pattern defines a family of algorithms, encapsulates each, and makes them interchangeable. A strategy lets the algorithm vary independent from clients that use it
Before we take that definition apart, let’s take a look at the object oriented principles that lead to this pattern:
Modifying code is usually pretty dangerous, because it can introduce regressions in your code.
If we do not properly encapsulate our code, then simple changes can have far reaching impact.
Accordingly, we would prefer to identify behavior that may vary in our code and isolate it from code that does not vary.
If we do this successfully, then the non varying code can be written, tested, and left alone forever.
…assuming our testing was sufficient
For instance, let’s assume we have the following code
public class Duck{
private final int id;
public Duck(final int id{
this.id = id;
}
public String fly(){
// Let's imagine this is a more complex computation
return "I'm flying";
}
public int getId(){ return id; }
}
In this example, we can be relatively sure that the id related code will not change. However, it is not unlikely that different ducks might fly differently.
So, we might do this instead
public class Duck{
private final int id;
private final FlyBehavior flyer = new FlyBehavior();
public Duck(final int id){
this.id = id;
}
public String fly(){ return flyer.fly(); }
public int getId(){ return id; }
}
public class FlyBehavior{
public String fly(){
return "I'm flying";
}
}
Now, if the flying behavior needs to change, we still never have to handle the Duck class again (almost…).
We still have a slight problem. Our Duck can only store the class FlyBehavior, so the result isn’t that flexible.
How can we support different flying behaviors?
If we write code which uses specific, concrete types, we are stuck with those types and have to manually modify our code, duplicate code and do other awful things to use other types.
If, instead, we program to more abstract interfaces, we can modify the behavior of our code, without modifying the code itself.
Note that, in this context, interface refers to a conceptual interface which can be coded in the form of
public class Duck{
private final int id;
private final FlyBehavior flyer;
public Duck(final int id, final FlyBehavior flyer){
this.id = id;
this.flyer = flyer;
}
public String fly(){ return flyer.fly(); }
public int getId(){ return id; }
}
Now, we can create various subclasses for FlyBehavior and swap them in and out to customize how ducks fly.
Even cooler, we can do this at runtime!
When we want to extend the behavior of our code, we typically have two ways to do it. We can either use inheritance or composition.
Using inheritance, A class A can extend another class B and override or add methods to obtain the desired behavior.
We often say that such an instance of A “is a” B.
We could instead compose objects and include a field of type C inside of a class B and the use that to change the behavior of the class B.
In this case, we would say that B “has a” C
We will learn that it is often valuable to write software so that different components are “decoupled” from each other. This is almost always easier when using composition.
At this point, we have reached a nice clean solution which is, in name, the strategy pattern.
As you see, we were able to reach this point only using basic principles, but it would have been easier to just jump straight to this design!
You should consider using the strategy pattern when:
Some drawbacks of the strategy pattern include:
We know that an interface that contains a single method can be replaced with a lambda expression.
This means we don’t have to define a special interface for the strategy: we just need to specify a general function interface as a parameter.
Additionally, we are passing strategies into constructors, but we could simply pass them into the methods where they are needed.
Storing them in a field is just a convenience.
At this point, we have essentially reached the basic concept of higher ordered functions
A higher ordered function is a function which takes another function as a parameter
If the strategy pattern is basically just a complicated implementation of higher ordered functions, then what is the point of jumping through extra hoops?
As Java incorporates more functional concepts, design patterns like this start to become much less interesting.
For this course, being comfortable with object oriented programming concepts and having a good understand of why they are beneficial is required.
Let’s start with interfaces!
First, what are interfaces?
We could say that interfaces are “contracts” between the implementers of some code and the users of that code.
In other words, in an interface, you are stating what methods you guarantee will be provided by any class implementing that interface.
Next, how do interfaces differ from classes?
The old school explanation would be that interfaces can not contain implementations of methods. They may only contain method signatures.
(This isn’t actually true anymore, though)
public interface Transform {
String apply(final String input);
}
This interface represents the abstract concept of code which performs String transformations.
Note that this code does not do anything! It just states that any class implementing this interface will provide a method called apply that will perform a String transformation.
Now how about classes?
We mentioned above that classes contain actual implementation details. In other words, they contain code that actually does stuff.
public class Reverse {
public String transform(final String input){
final StringBuilder sb = new StringBuilder();
for(int i=input.length-1; i>=0; i--){
sb.append(input.charAt(i));
}
return sb.toString();
}
}
Is this a class?
Does it perform a String transformation?
Does it implement the Transform interface?
public class Reverse implements Transform {
public String apply(final String input){
// ...
}
}
Now we have stated that we are agreeing to the “contract” defined by Transform
final Reverse transformer = new Reverse();
System.out.println(transformer.apply("Hello");
final Transform transformer = new Reverse();
System.out.println(transformer.apply("Hello"));
What’s the difference here? Why would we do this?
At this point, there is no real reason to use interfaces at all. What happens, though, when we decide to add another type of transformation to our code.
if(doReverse){
final Reverse reverse = new Reverse();
return reverse.apply("Hello");
}else if(doNoVowels){
final NoVowels noVowels = new NoVowels();
return noVowels.apply("Hello");
}else if ...
While all of these classes do different things, they all, at a high level, are transforming Strings.
So, if we use the Transform interface for all of them, then we can unify them with one variable.
final Transform transform;
if(doReverse){
transform = new Reverse();
}else if(doNoVowels){
transform = new NoVowels();
}else if ...
return transform.apply("Hello");
This may not seem to impressive, but what if the logic choosing the transformation is not in our code?
What if we have to pass a transform into someone elses code?
What if we need a collection of transformations?
final SomeoneElses code = new SomeoneElsesCode();
// We have no idea what this could possibly return!
final Transform transform = code.getTransform();
// ...but we can use it anyway, because we know its interface!
return transform.apply("Hello");
In someone else’s code:
public void setTransform(final Transform t){
transform = t;
}
In our code:
//They have no idea what this is
final Transformer tx = new Reverse();
//...but they can still use it!
code.setTransform(tx);
final List<Transform> txs = new ArrayList<>();
txs.add(new Reverse());
txs.add(new NoVowels());
//...
What are all of the angle brackets?
final List<Transform> txs = new ArrayList<>();
I guess we’re going to need to talk about generics.
We mentioned how an interface can be used to represent different classes by referring to them as instances of the interface instead.
We can also do this with an ancestor classes, if all of the classes have a common ancestor. So, in the bad old days, a List would simply store Objects, because Object is a common ancestor for all other classes.
This led to problems:
final List txs = new ArrayList();
txs.add(new Reverse());
// Not an error, because it's an object
txs.add("Hello");
// Have to cast, because we need a Transform, not an Object
final Transform tx = (Transform)txs.get(1);
To fix this, generics were introduced to allow for more type safe code to be written:
final List<Transform> txs = new ArrayList<>();
txs.add(new Reverse());
//Now this is an error
txs.add("Hello");
//And a cast wouldn't be needed if the above didn't crash!
final Transform tx = txs.get(0);
Let’s improve our Transform interface now:
public interface Transform<T,U> {
U apply(final T input);
}
Now we’re not limited to String transformations!
public class Reverse implements Transform<String,String> {
public String apply(final String input){
//...
}
}
final List<Transform<String,String>> stringTransformers;
final List<Transform<Integer,Integer>> intTransformers;
final List<Transform<String,Integer>> stringToIntTransformers;
Can you feel the power??? Programming is so awesome.
Ok, let’s dial things back a bit and get back to some basics.
What is inheritance? How does it work?
Previously, we implemented an interface.
Sometimes, though, we want to extend a class.
When a class B extends a class A, then the class B will inherit the methods and fields of class A.
public class ReReverse extends Reverse {
}
What will the following do? Will it compile?
final Reverse r1 = new ReReverse();
System.out.println(r1.apply("hello");
public class ReReverse extends Reverse {
public String apply(final String input){
final String reversed = super.apply(input);
final String reReversed = super.apply(reveresed);
return reversed;
}
}
Super?
What will the following do?
final Reverse r1 = new ReReverse();
System.out.println(r1.apply("hello");
final ReReverse r2 = new Reverse();
System.out.println(r2.apply("hello");
final Transform t = new ReReverse();
System.out.println("hello");
When we declare a variable’s type, that tells us what fields and methods must exist, but it might not be the precise type of the value bound to that variable.
(The actual value may have more fields and methods!)
When we call a method declared in the parent class, the JVM will determine, at run time, what the actual type of the value is, and will use this information to select the correct implementation of the called method.
What is the correct implementation?
If the child class has not overridden the method, then the parent class will be checked for an implementation.
If the child class has overriden the method, then the implementation in the child class is chosen.
Remember, though, that you can have several levels of inheritance!
Don’t forget that access modifiers affect what fields and methods are visible to children!
If, for example, a parent class declares a private field, the child can not access it!
none | private | protected | public | |
---|---|---|---|---|
class | Y | Y | Y | Y |
package | Y | N | Y | Y |
subclass | N | N | Y | Y |
world | N | N | N | Y |
A common mistake students (and professionals!) make is to assume that the access modifiers apply to instances of a class. This is not true!
For instance, a private field in class A is visible ot every instance of A!
For example, this code is perfectly valid!
public class A {
private final int x;
//...
public void compareXs(final A anotherA){
// works even though x is private!
return x==anotherA.x;
}
}
Let’s circle back around to some of the basics:
An interface (sort of) only provides method signatures.
A class must be completely implemented.
An abstract class is when you want something in between.
public abstract class Censor
implements Transform<String,String> {
public abstract List<String> getBadWords();
public String apply(String input){
for(final String word : badWords){
input = input.replace(word, "!!!")
}
return input;
}
}
Now we can create different variations by subclassing and implementing getBadWords.
What if we want to inherit from two classes?
You can’t!
You can implement several interfaces, but you can only extend one class (concrete or abstract).
However…
You can now, actually, provide “default” implementations of methods in interfaces.
Which means that you can often accomplish what you were trying to do with two parent classes by instead implementing two interfaces with default methods.
There are, however, limitations. First, remember that you can only store static, constant fields in an interface. If you need instance data for your default methods, you are out of luck.
You can, though, add a getter to the interface for the instance data, and use that in your default method.
Then implementers will declare their own instance fields and implements the getter.
public interface Censor extends Transform<String,String> {
List<String> getBadWords();
default String apply(String input){
for(final String bad : getBadWords()){
input = input.replace(bad, "!!!");
}
return input;
}
}
Another common mistake is to try and implement an interface in another interface. This makes no sense, because you aren’t (generally) providing an implementation!
So, interfaces extend other interfaces!
An interesting use for multiple interfaces is to create an interface for each kind of behavior that might be needed, and mixing them together.
public interface Labeled {
public String getLabel();
}
public interface Logged {
public List<String> getLog();
}
public class Reverse
implements Transform<String,String>, Labeled, Logged {
private final List<String> log = new ArrayList<>();
public String getLabel(){return "Reverse (Transform)";}
public List<String> getLog(){return log;}
public String apply(final input){
log.add("Starting transform of input: " + input);
//...
log.add("Transform complete:" + result);
return result;
}
}
So, in addition to being able to use this class wherever we need a Transform, we can use it anywhere we expect, for example, instances with a label.
For example, imagine a UI where we can select operations to perform. Some might be transformers, other might not be, but we can put them all in a common UI widget with a nicely displayed label if they all implement the Labeled interface!
At runtime, though, we may not know what interfaces are implemented, so we will need to check using the instanceof operator.
public void doTransform(final Transform<String,String> t){
final String input = getInput();
final String output = t.apply(input);
if(t instanceof Logged){
final Logged logged = (Logged) t;
printLog(logged.getLog());
}
writeOutput(output);
}
In this appendix, we will discuss interfaces, anonymous classes, and lambda expressions which are becoming a more commonly used feature in Java (and other languages) and allow for writing incredibly succinct, clear code.
In order to understand lambda expressions, we will start with interfaces and slowly work our way to proper lambda expressions.
Let’s use the Transform interface from our Object Oriented review:
public interface Transform {
String apply(final input String);
}
We can use this interface to implement types of Transformers.
public class ToCaps implements Transform {
public String apply(final String input){
return input.toUpperCase();
}
}
Once we have a concrete class defined, we would typically create and use instances as follows:
final Transform tx = new ToCaps();
final String result = tx.apply("hello");
return result;
Sometimes, though, we will only be creating instances for a class in one location in our code. Creating another file and another class is overkill, so instead we could use an anonymous class.
final Transform tx = new Transform(){
public String apply(final String input){
return input.toUpperCase();
}
};
final String result = tx.apply("hello");
return result;
Basically, this allows us to declare the same class inline.
It’s “anonymous,” because it no longer has a name.
(it doesn’t need one, because we are only referencing this class here!)
If you look at the declaration of the anonymous class, you see that we have declared the variable to be a Transform, so in theory, we shouldn’t have to tell the compiler we are creating a Transform.
This won’t actually compile, but you should be able to see how the compiler could be implemented so that it would.
final Transform tx = {
public String apply(final String input){
return input.toUpperCase();
}
};
final String result = tx.apply("hello");
return result;
Also, since there is only one method in the Transform interface, it seems like we should also be able to leave the method name, parameter type, return type, and access modifier out.
final Transform tx = {
(input){
return input.toUpperCase();
}
};
final String result = tx.apply("hello");
return result;
Now, if we just eliminate unnecessary braces and tweak the syntax a bit, we can arrive at the following, which will compile.
final Transform tx = (input)->{
return input.toUpperCase();
};
final String result = tx.apply("hello");
return result;
In this particular case, since there is only a single statement in the method body, we can be even more concise.
final Transform tx = (input)->input.toUpperCase();
final String result = tx.apply("hello");
return result;
And since there is only a single parameter, we can do a little better.
final Transform tx = input->input.toUpperCase();
final String result = tx.apply("hello");
return result;
This may not seem that interesting, but let’s look at a more complex example:
public class Transformer {
private final Transform transform;
public Transformer(final Transform tx){
this.transform = tx;
}
public List<String> transform(final Source source){
final List<String> output = new ArrayList<>();
for(final String input : source){
output.add(transform.apply(input));
}
return output;
}
}
Now we can write really succinct code like:
private final Source = getSource();
final Transformer toUpper =
new Transformer(s->s.toUpperCase());
toUpper.transform(source);
final Transformer toLower =
new Transformer(s->s.toLowerCase());
toLower.transform(source);
…or maybe you have experienced the pain of writing a bunch of ActionListeners?
button.addActionListener(
event->JOptionPane.showMessageDialog(this, "I was clicked!")
);
In addition to lambda expressions we can also use method references to avoid even more boiler plate code!
Which gives us:
final Transformer toUpper =
new Transformer(String::toUpperCase);
Instead of
final Transformer toUpper =
new Transformer(s -> s.toUpperCase());
The most generalized version of this code would not even specify that a Transform is needed. Instead, the constructor would be:
public Transformer(Function<String,String> f){
//...
}
Java provides Function and BiFunction interfaces to represent any one argument or two argument method, class, etc.
For zero argument functions, Java provides the Supplier interface:
final Supplier<String> s = ()->"Hello!";
And for a “function” with no return, the Consumer interface:
final Consumer<String> c = s->System.out.println(s);
If you want to pass a computation that has no parameters and no output, then you can use the Runnable interface:
new Thread(
()->{
while(true){
System.out.println("JavaScript Sucks");
}
}
).start();
One other interesting thing you can do with anonymous classes and lambda expressions is use them to capture variables in their closures.
public Transformer create(String message, int count){
return new Transformer(input->{
final StringBuilder sb = new StringBuilder();
for(int i=0; i<count; i++){
sb.append(text);
}
});
}
final Transformer t = create("hi",4);
t.transform(source); //message and count are out of scope!?
This works, because any variable referenced in the lambda expression is captured and will be accessible for the life of the expression!
I call it my billion-dollar mistake. It was the invention of the null reference in 1965. […] I couldn’t resist the temptation to put in a null reference, simply because it was so easy to implement.
In reality, 1 billion dollars is a huge understatement of the financial damage caused by null references.
Most developers spend a comically large amount of time chasing null pointers.
…but you don’t have to!
This one secret will save you time and money–more than the cost of this course!
Many will tell you that null pointers aren’t a problem if you’re a good programmer.
final User user = getUser(10);
final String name;
if(user !=null){
name = user.getName();
}else{
name = "Not Found";
}
Problem: Every method could, possibly, return a null value, so to be safe you need to check the result of every method.
The “good programmers” don’t do this, though. They “know” which methods need to be tested…which is why they, too, get null pointer exceptions.
Problem: If you don’t want to test every method, you need to read the documentation for methods to determine which ones can return null.
The “good programmers” don’t do this either…which is why they, too, get null pointer exceptions.
Problem: If you do read the documentation, it is imperative that the documentation is correct and complete.
The “good programmers” don’t write and update documentation sufficiently, though…which is why they, too, get null pointer exceptions.
Of course, the “good programmers” always have excuses for why their null pointer problems are not their fault and continue to have null pointer problems.
Meanwhile, the “incompetent” programmer acknowledges his or her inability to cope with complex systems and asks the compiler for help.
public Optional<User> getUser(int id){
if(userExists(id)){
return Optional.of(new User(id));
}else{
return Optional.empty();
}
}
Now, this won’t even compile!
final User user = getUser();
The compiler complains, because the return value is not a User. But we need an User…
final Optional<User> maybeUser = getUser(10);
final String name;
if(maybeUser.isPresent()){
name = maybeUser.get().getName();
}else{
name = "Not Found";
}
Did we just do the same thing as the good programmer?
Sort of. There is one immediate difference, though: Now we are forced by the compiler to address the possibility of no value being returned.
This is already a big gain with only slightly more code needed than the good programmer’s approach.
If your team is on board with never returning null values (which is significantly easier than avoiding null values returned from methods unexpectedly), then you can use Optional values to eliminate null pointer exceptions from your code.
Java, unfortunately, does not prevent you from returning a null value even if the return type is an Optional value.
You should have used Haskell.
Another bonus to this approach is that the return type itself documents the fact that the method may not return a value. Even better, if this documentation changes, the compiler will force you to rethink your use of the method!
Of course, you could just assume that the method returns a value and write:
final String name = getUser(10).get().getName();
Which isn’t any better than
final String name = getUser(10).getName()
Using the old code.
The big difference, though, is psychology. If you know the meaning of an Optional return type, and you decide that, even though this method is guaranteed to sometimes not return a value, you are going to roll the dice, then there is literally no programming language feature that can help you, because you willfully do bad things.
On the other hand, the developer who doesn’t check for a null is often just being careless. There is no red flag being waived in his or her face about the certain danger.
So, Optional return types flip the responsibility around.
With null values, you have to be responsible enough to make sure you don’t screw up.
With Optional values, you are almost forced to write good, safe code and have to male a conscious decision to do the bad thing.
(Some even argue that get() should not exist in the Optional class)
Problem: Why are we returning a default value of “Not Found”? If the User does not exist,
then why should the name exist? We could just return a null
, but that’s bad!
Instead, we should just return an Optional name!
final Optional<User> maybeUser = getUser(10);
if(maybeUser.isPresent()){
final User u = maybeUser.get();
return Optional.of(u.getName());
}else{
return Optional.empty();
}
At this point you should be ready to revolt and assume I am trolling you.
Are you really, supposed to litter Optional wrappers throughout every line of code?
No! Higher order functions will rescue us and make life beautiful.
The Optional class defines a method called map which accepts a function as its argument.
More specifically, for the type Optional of type T, it accepts a function that takes an object of type T and returns some other type of object.
Using this, we can write the following instead:
return getUser(10).map(u->u.getName());
If the Optional contains a value, the function is applied to it, resulting in an Optional containing an String. Otherwise, the whole thing is just an empty Optional.
Now, we have obtained safety and simpler code. Good programmers and their null checks have been thoroughly defeated at this point.
What if the getName() method itself returns an *Optional
final Optional<String> result =
getUser(10).map(u->u.getName());
This doesn’t compile. Why?
The Optional type also provides a method flatMap that takes a function which returns an Optional value and “flattens” the Optional layers so that there are no nested Optionals
Now, we can write safe code in the face of missing values quite simply:
return getUser(10)
.flatMap(u -> u.getName())
.map(n -> n.toUpperCase())
.filter(n -> n.equals("PHILLIP"))
.map(n->"User: " + n)
We can do a little cleanup using method references
return getUser(10)
.flatMap(User::getName)
.map(String::toUpperCase)
.filter(n -> n.equals("PHILLIP"))
.map(n->"User: " + n)