JavaRush /Java Blog /Random EN /Coffee break #155. Top 10 Functions in Java

Coffee break #155. Top 10 Functions in Java

Published in the Random EN group

Top 10 Functions in Java

Source: DZone This article lists ten Java programming features that are often used by developers in their daily work. Coffee break #155.  Top 10 functions in Java - 1

1. Collection Factory Method

Collections are one of the most commonly used features in programming. They are used as a container in which we store objects and pass them on. Collections are also used to sort, search, and repeat objects, making a programmer's life much easier. They have several basic interfaces such as List, Set, Map, as well as several implementations. The traditional way of creating Collections and Maps can seem verbose to many developers. That's why Java 9 introduced several concise factory methods. List :
List countries = List.of("Bangladesh", "Canada", "United States", "Tuvalu");
Set :
Set countries = Set.of("Bangladesh", "Canada", "United States", "Tuvalu");
Map :
Map countriesByPopulation = Map.of("Bangladesh", 164_689_383,
                                                            "Canada", 37_742_154,
                                                            "United States", 331_002_651,
                                                            "Tuvalu", 11_792);
The factory method is very useful when we want to create immutable containers. But if you are going to create mutable collections, it is recommended to use the traditional approach.

2. Local Type Inference

Java 10 added type inference for local variables. Before this, developers had to specify types twice when declaring and initializing an object. It was very tiring. Look at the following example:
Map> properties = new HashMap<>();
The type of information on both sides is indicated here. If we define it in one place, then the code reader and the Java compiler will easily understand that it must be a Map type. Local type inference does just that. Here's an example:
var properties = new HashMap>();
Now everything is written only once and the code doesn’t look much worse. And when we call a method and store the result in a variable, the code becomes even shorter. Example:
var properties = getProperties();
And further:
var countries = Set.of("Bangladesh", "Canada", "United States", "Tuvalu");
Although local type inference seems like a convenient feature, some people criticize it. Some developers argue that this reduces readability. And this is more important than brevity.

3. Advanced Switch Expressions

The traditional switch statement has been around in Java since the beginning and was reminiscent of C and C++ back then. This was fine, but as the language evolved, this operator did not offer us any improvements until Java 14. Of course, it had some disadvantages. The most notorious was fall -through: To solve this problem, developers used break statements, which are largely boilerplate code. However, Java 14 introduced an improved version of the switch statement with a much larger list of functions. Now we no longer need to add break statements and this solves the failure problem. Additionally, a switch statement can return a value, which means we can use it as an expression and assign it to a variable.
int day = 5;
String result = switch (day) {
    case 1, 2, 3, 4, 5 -> "Weekday";
    case 6, 7 -> "Weekend";
    default -> "Unexpected value: " + day;
};

4. Records

Although Records is a relatively new feature introduced in Java 16, many developers find it very useful, mainly due to the creation of immutable objects. Often we need data objects in our program to store or pass values ​​from one method to another. For example, a class for transferring x, y and z coordinates, which we will write as follows:
package ca.bazlur.playground;

import java.util.Objects;

public final class Point {
    private final int x;
    private final int y;
    private final int z;

    public Point(int x, int y, int z) {
        this.x = x;
        this.y = y;
        this.z = z;
    }

    public int x() {
        return x;
    }

    public int y() {
        return y;
    }

    public int z() {
        return z;
    }

    @Override
    public boolean equals(Object obj) {
        if (obj == this) return true;
        if (obj == null || obj.getClass() != this.getClass()) return false;
        var that = (Point) obj;
        return this.x == that.x &&
                this.y == that.y &&
                this.z == that.z;
    }

    @Override
    public int hashCode() {
        return Objects.hash(x, y, z);
    }

    @Override
    public String toString() {
        return "Point[" +
                "x=" + x + ", " +
                "y=" + y + ", " +
                "z=" + z + ']';
    }

}
The class seems too verbose. With the help of entries, all this code can be replaced with a more concise version:
package ca.bazlur.playground;

public record Point(int x, int y, int z) {
}

5.Optional

A method is a contract in which we define conditions. We specify the parameters with their type, as well as the return type. We then expect that when the method is called, it will behave according to the contract. However, often we end up with null from a method instead of a value of the specified type. This is mistake. To resolve this, the initiator typically tests the value with an if condition, regardless of whether the value is null or not. Example:
public class Playground {

    public static void main(String[] args) {
        String name = findName();
        if (name != null) {
            System.out.println("Length of the name : " + name.length());
        }
    }

    public static String findName() {
        return null;
    }
}
Look at the above code. The findName method is supposed to return a String , but it returns null. The initiator must now check for nulls first to deal with the problem. If the initiator forgets to do this, then we will end up getting a NullPointerException . On the other hand, if the method signature indicated the possibility of non-return, then this would solve all the confusion. And this is where Optional can help us .
import java.util.Optional;

public class Playground {

    public static void main(String[] args) {
        Optional optionalName = findName();
        optionalName.ifPresent(name -> {
            System.out.println("Length of the name : " + name.length());
        });
    }

    public static Optional findName() {
        return Optional.empty();
    }
}
Here we've rewritten the findName method with an Optional option to not return any value. This alerts programmers in advance and fixes the problem.

6. Java Date Time API

Every developer is confused to one degree or another with calculating date and time. This is not an exaggeration. This was mainly due to the lack of a good Java API for working with dates and times. Now this problem is no longer relevant, because Java 8 introduced an excellent set of APIs in the java.time package, which solves all issues related to date and time. The java.time package has many interfaces and classes that eliminate most problems, including time zones. The most commonly used classes in this package are:
  • LocalDate
  • LocalTime
  • LocalDateTime
  • Duration
  • Period
  • ZonedDateTime
An example of using classes from the java.time package:
import java.time.LocalDate;
import java.time.Month;

public class Playground3 {
    public static void main(String[] args) {
        LocalDate date = LocalDate.of(2022, Month.APRIL, 4);
        System.out.println("year = " + date.getYear());
        System.out.println("month = " + date.getMonth());
        System.out.println("DayOfMonth = " + date.getDayOfMonth());
        System.out.println("DayOfWeek = " + date.getDayOfWeek());
        System.out.println("isLeapYear = " + date.isLeapYear());
    }
}
An example of using the LocalTime class to calculate time:
LocalTime time = LocalTime.of(20, 30);
int hour = time.getHour();
int minute = time.getMinute();
time = time.withSecond(6);
time = time.plusMinutes(3);
Adding a time zone:
ZoneId zone = ZoneId.of("Canada/Eastern");
LocalDate localDate = LocalDate.of(2022, Month.APRIL, 4);
ZonedDateTime zonedDateTime = date.atStartOfDay(zone);

7.NullPointerException

Every developer hates NullPointerException. It can be especially difficult when StackTrace doesn't provide useful information about what exactly the problem is. To demonstrate this, let's take a look at the sample code:
package com.bazlur;

public class Main {

    public static void main(String[] args) {
        User user = null;
        getLengthOfUsersName(user);
    }

    public static void getLengthOfUsersName(User user) {
        System.out.println("Length of first name: " + user.getName().getFirstName());
    }
}

class User {
    private Name name;
    private String email;

    public User(Name name, String email) {
        this.name = name;
        this.email = email;
    }

   //getter
   //setter
}

class Name {
    private String firstName;
    private String lastName;

    public Name(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }

   //getter
   //setter
}
Look at the basic method in this passage. We see that a NullPointerException will be thrown next . If we run and compile the code in a version before Java 14, we will get the following StackTrace:
Exception in thread "main" java.lang.NullPointerException
at com.bazlur.Main.getLengthOfUsersName(Main.java:11)
at com.bazlur.Main.main(Main.java:7)
There is very little information here about where and why the NullPointerException occurred . But in Java 14 and later versions, we get much more information in StackTrace, which is very convenient. In Java 14 we will see:
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "ca.bazlur.playground.User.getName()" because "user" is null
at ca.bazlur.playground.Main.getLengthOfUsersName(Main.java:12)
at ca.bazlur.playground.Main.main(Main.java:8)

8. CompletableFuture

We write programs line by line, and they are usually executed line by line. But there are times when we need parallel execution to make the program faster. For this we usually use Java Thread. Java thread programming is not always about parallel programming. Instead, it gives us the ability to compose several independent program modules that will execute independently and often even asynchronously. However, thread programming is quite difficult, especially for beginners. This is why Java 8 offers a simpler API that allows you to execute part of a program asynchronously. Let's see an example. Let's say we need to call three REST APIs and then combine the results. We can call them one by one. If each of them takes about 200 milliseconds, then the total time to receive them will take 600 milliseconds. What if we could run them in parallel? Since modern processors are typically multi-core, they can easily handle three rest calls on three different processors. Using CompletableFuture we can do this easily.
package ca.bazlur.playground;

import java.time.Duration;
import java.time.Instant;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;

public class SocialMediaService {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        var service = new SocialMediaService();

        var start = Instant.now();
        var posts = service.fetchAllPost().get();
        var duration = Duration.between(start, Instant.now());

        System.out.println("Total time taken: " + duration.toMillis());
    }

    public CompletableFuture> fetchAllPost() {
        var facebook = CompletableFuture.supplyAsync(this::fetchPostFromFacebook);
        var linkedIn = CompletableFuture.supplyAsync(this::fetchPostFromLinkedIn);
        var twitter = CompletableFuture.supplyAsync(this::fetchPostFromTwitter);

        var futures = List.of(facebook, linkedIn, twitter);

        return CompletableFuture.allOf(futures.toArray(futures.toArray(new CompletableFuture[0])))
                .thenApply(future -> futures.stream()
                        .map(CompletableFuture::join)
                        .toList());
    }
    private String fetchPostFromTwitter() {
        sleep(200);
        return "Twitter";
    }

    private String fetchPostFromLinkedIn() {
        sleep(200);
        return "LinkedIn";
    }

    private String fetchPostFromFacebook() {
        sleep(200);
        return "Facebook";
    }

    private void sleep(int millis) {
        try {
            TimeUnit.MILLISECONDS.sleep(millis);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}

9. Lambda Expressions

Lambda expressions are perhaps the most powerful feature of the Java language. They changed the way we write code. A lambda expression is like an anonymous function that can take arguments and return a value. We can assign a function to a variable and pass it as arguments to a method, and the method can return it. He has a body. The only difference from the method is that there is no name. Expressions are short and concise. They usually do not contain a lot of boilerplate code. Let's see an example where we need to list all the files in a directory with .java extension.
var directory = new File("./src/main/java/ca/bazlur/playground");
String[] list = directory.list(new FilenameFilter() {
    @Override
    public boolean accept(File dir, String name) {
        return name.endsWith(".java");
    }
});
If you look closely at this piece of code, we have passed the anonymous inner class list() to the method . And in the inner class we placed the logic for filtering files. Essentially, we are interested in this part of the logic, not the pattern around the logic. The lambda expression allows us to remove the entire template and we can write the code that interests us. Here's an example:
var directory = new File("./src/main/java/ca/bazlur/playground");
String[] list = directory.list((dir, name) -> name.endsWith(".java"));
Of course, this is just one example; lambda expressions have many other benefits.

10. Stream API

In our daily work, one of the common tasks is processing a data set. It has several common operations such as filtering, transforming, and collecting results. Before Java 8, such operations were imperative in nature. We had to write code for our intent (i.e., what we wanted to achieve) and how we would like to do it. With the invention of the lambda expression and the Stream API, we can now write data processing functions declaratively. We only indicate our intention, and we do not need to write down how we get the result. Here's an example: We have a list of books and we want to find all the names of the Java books, separated by commas and sorted.
public static String getJavaBooks(List books) {
    return books.stream()
            .filter(book -> Objects.equals(book.language(), "Java"))
            .sorted(Comparator.comparing(Book::price))
            .map(Book::name)
            .collect(Collectors.joining(", "));
}
The above code is simple, readable and concise. But below you can see an alternative imperative code:
public static String getJavaBooksImperatively(List books) {
    var filteredBook = new ArrayList();
    for (Book book : books) {
        if (Objects.equals(book.language(), "Java")){
            filteredBook.add(book);
        }
    }
    filteredBook.sort(new Comparator() {
        @Override
        public int compare(Book o1, Book o2) {
            return Integer.compare(o1.price(), o2.price());
        }
    });

    var joiner = new StringJoiner(",");
    for (Book book : filteredBook) {
        joiner.add(book.name());
    }

    return joiner.toString();
}
Although both methods return the same value, we can clearly see the difference.
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION