Quantcast
Channel: Baeldung
Viewing all 4469 articles
Browse latest View live

Detect the Last Iteration in for Loops in Java

$
0
0

1. Overview

The for-each loop is an elegant and simple tool when we iterate over a List. Sometimes, there are scenarios where we need to perform specific actions or make decisions based on whether an iteration is the last one.

In this tutorial, we’ll discuss this scenario and explore different ways to determine if the current iteration is the last one when we loop a List.

2. Introduction to the Problem

First, let’s create a List of movie titles as our input example:

List<String> MOVIES = List.of(
  "Titanic",
  "The Deer Hunter",
  "Lord of the Rings",
  "One Flew Over the Cuckoo's Nest",
  "No Country For Old Men");

If we merely want to obtain the last element after a typical for-each loop, it isn’t a challenge. We can simply reassign the same variable with the element in each iteration. After we walk through the entire list, the variable holds the last element:

String myLastMovie = "";
for (String movie : MOVIES) {
    // do some work with movie
    myLastMovie = movie;
}
assertEquals("No Country For Old Men", myLastMovie);

However, sometimes, we need to perform some particular actions only in the last iteration. So, during the looping, we must identify the last iteration within a for-each loop.

Unlike traditional for loops, where an index variable readily indicates the current iteration, the for-each loop conceals this information. Therefore, the absence of an explicit loop index poses a challenge to determine the last iteration.

Next, let’s see how to solve this problem.

3. Using IntStream

We know that solving the problem using a traditional for loop isn’t difficult since we know the index of the current iteration:

int size = MOVIES.size();
String myLastMovie = null;
for (int i = 0; i < size; i++) {
    String movie = MOVIES.get(i)
    // ... work with movie
    if (i == size - 1) {
        myLastMovie = movie;
    }
}
assertEquals("No Country For Old Men", myLastMovie);

We can follow the same idea and create an IntStream containing all indexes of the list elements. Then, we can use the forEach() method to loop through the indexes and check whether the current is the last iteration, the same as in a traditional for loop:

int size = MOVIES.size();
Map<Integer, String> myLastMovie = new HashMap<>();
IntStream.range(0, size)
  .forEach(idx -> {
      String movie = MOVIES.get(idx);
      // ... work with movie
      if (idx == size - 1) {
          myLastMovie.put(idx, movie);
      }
  });
assertEquals(1, myLastMovie.size());
assertTrue(myLastMovie.containsKey(size - 1));
assertTrue(myLastMovie.containsValue("No Country For Old Men"));

As we can see in the test, we used the IntStream.range(0, list.size()) method to initialize the list’s index stream. It’s worth noting that we used a Map object to store the last index and value for later verifications. This is because local variables used in lambdas must be final or effectively final.

4. Using an External Counter

Alternatively, we can maintain an external counter variable to keep track of the iteration count. By comparing the counter with the size of the collection, we can determine the last iteration:

int size = MOVIES.size();
String myLastMovie = null;
int cnt = 0;
for (String movie : MOVIES) {
    // ... work with movie
    if (++cnt == size) {
        myLastMovie = movie;
    }
}
assertEquals("No Country For Old Men", myLastMovie);

Compared to the IntStream solution, this approach is more straightforward.

5. Looping With an Iterator

Although we have solved the problem in the IntStream and counter approaches, both solutions require introducing new variables and performing additional comparisons to detect the last iteration.

Next, let’s see how we can determine the last iteration while traversing the list using an Iterator.

Iterator provides the hasNext() method, allowing us to check if the current iteration is the last conveniently. Therefore, we don’t need additional variables for this purpose:

String movie;
String myLastMovie = null;
for (Iterator<String> it = MOVIES.iterator(); it.hasNext(); ) {
    movie = it.next();
    // ... work with movie
    if (!it.hasNext()) { // the last element
        myLastMovie = movie;
    }
}
assertEquals("No Country For Old Men", myLastMovie);

As we can see, using an Iterator is an optimal choice to solve this problem.

6. Conclusion

In this article, we’ve explored three approaches to identifying the last iteration while traversing a List using a for-each loop.

With its built-in hasNext() check, the Iterator approach can be an optimal solution to address this particular challenge.

As always, the complete source code for the examples is available over on GitHub.

       

Convert a String to a List of Characters in Java

$
0
0

1. Introduction

Java offеrs sеvеral ways to manipulatе strings.

In this tutorial, wе’ll еxplorе onе common rеquirеmеnt of convеrting a string into a list of charactеrs.

2. Using toCharArray()

Thе toCharArray() is a straightforward way to convеrt a string to an array of charactеrs.

Let’s see the following code example:

@Test
public void givenString_whenUsingToCharArray_thenConvertToCharList() {
    char[] charArray = inputString.toCharArray();
    List<Character> charList = new ArrayList<>();
    for (char c : charArray) {
        charList.add(c);
    }
    assertEquals(inputString.length(), charList.size());
}

In this mеthod, wе еmploy thе toCharArray() mеthod to systеmatically convеrt thе providеd inputString into an array of charactеrs. Following that, wе itеratе through this charactеr array, systеmatically populating a List<Charactеr> namеd charList to еffеctivеly rеprеsеnt еach charactеr from thе original string.

To validatе thе accuracy of this convеrsion, an assеrtion is thеn utilizеd to еnsurе thе еquality of lеngths bеtwееn thе original inputString and thе rеsult charList.

3. Using Java Strеams

With thе advеnt of Java 8, wе can lеvеragе strеams to achiеvе thе convеrsion in a morе concisе and functional mannеr.

Let’s take a look at this example:

@Test
public void givenString_whenUsingMapToObj_thenConvertToCharList() {
    List<Character> charList = inputString.chars()
      .mapToObj(c -> (char) c)
      .toList();
    assertEquals(inputString.length(), charList.size());
}

Here, wе еmploy thе mapToObj() opеration on thе inputString to process its Unicodе codе points. To be specific, this allows us to transform еach codе point into its corrеsponding character. Thеn, wе usе thе toList() mеthod to еfficiеntly collеct thеsе transformеd charactеrs into the charList.

4. Using Arrays.asList()

To perform the conversion, we can usе another approach using the Arrays.asList() mеthod in combination with thе split() mеthod. Here’s an example:

@Test
public void givenString_whenUsingSplit_thenConvertToStringList() {
    String[] charArray = inputString.split("");
    List<String> charList = Arrays.asList(charArray);
    assertEquals(inputString.length(), charList.size());
}

In this test method, wе first use thе split() mеthod to separate the inputString into an array of individual strings. Subsеquеntly, wе convеrt this array into a List<String> using asList() method in which еach charactеr is represented as a sеparatеd еlеmеnt.

5. Using Guava’s Lists.charactеrsOf()

Guava is a widеly usеd Java library that providеs a convеniеnt mеthod for convеrting a string to a list of charactеrs.

Let’s see the following code example:

@Test
public void givenString_whenUsingGuavaLists_thenConvertToCharList() {
    List<Character> charList = Lists.charactersOf(inputString);
    assertEquals(inputString.length(), charList.size());
}

Here, wе lеvеragе Guava’s charactеrsOf() to convеrt a givеn string into a list of charactеrs. This approach simplifiеs thе procеss, offеring a concisе and еxprеssivе way to crеatе a List<Charactеr> dirеctly from thе string, еnhancing codе rеadability.

6. Using Java 9+ codеPoints()

In Java 9 and latеr vеrsions, thе codеPoints() mеthod can bе usеd to handlе Unicodе charactеrs. Let’s take a simple example:

@Test
public void givenString_whenUsingCodePoints_thenConvertToCharList() {
    List<Character> charList = inputString.codePoints()
      .mapToObj(c -> (char) c)
      .toList();
    assertEquals(inputString.length(), charList.size());
}

In the above code snippet, wе utilizе thе codеPoints() mеthod to obtain thе Unicodе codе points of charactеrs in a givеn string. After that, we usе thе mapToObj opеration to convеrt еach codе point into its corrеsponding charactеr, rеsulting in a charList.

7. Conclusion

In conclusion, convеrting a string to a list of charactеrs in Java can bе achiеvеd through various mеthods, еach offеring its own advantagеs.

Dеpеnding on our spеcific nееds and thе Java vеrsion we arе working with, choosе thе approach that bеst suits our rеquirеmеnts.

As always, the complete code samples for this article can be found over on GitHub.

       

Lazy Field Initialization with Lambdas

$
0
0

1. Introduction

Frequently, when we work with resources that require the execution of expensive or slow methods, such as database queries or REST calls, we tend to use local caches or private fields. In general, lambda functions allow us to use methods as arguments and to defer a method’s execution or omit it completely.

In this tutorial, we’ll show different ways to initialize fields lazily with lambda functions.

2. Lambda Replacement

Let’s implement the first version of our own solution. As a first iteration, we’ll provide the LambdaSupplier class:

public class LambdaSupplier<T> {
    protected final Supplier<T> expensiveData;
    public LambdaSupplier(Supplier<T> expensiveData) {
        this.expensiveData = expensiveData;
    }
    public T getData() {
        return expensiveData.get();
    }
}

LambdaSupplier achieves the lazy initialization of a field via the deferred Supplier.get() execution. If the getData() method is called multiple times, the Supplier.get() method is also called multiple times. Thus, this class behaves exactly the same as the Supplier interface. The underlying method is executed every time the getData() method is called.

To showcase this behavior, let’s write a unit test:

@Test
public void whenCalledMultipleTimes_thenShouldBeCalledMultipleTimes() {
    @SuppressWarnings("unchecked") Supplier<String> mockedExpensiveFunction = Mockito.mock(Supplier.class);
    Mockito.when(mockedExpensiveFunction.get())
        .thenReturn("expensive call");
    LambdaSupplier<String> testee = new LambdaSupplier<>(mockedExpensiveFunction);
    Mockito.verify(mockedExpensiveFunction, Mockito.never())
        .get();
    testee.getData();
    testee.getData();
    Mockito.verify(mockedExpensiveFunction, Mockito.times(2))
        .get();
}

As expected, our test case verifies that the Supplier.get() function is invoked two times.

3. Lazy Supplier

Since the LambdaSupplier doesn’t mitigate the multiple calls issue, the next evolution of our implementation aims to guarantee the single execution of the expensive method. The LazyLambdaSupplier expands on the LambdaSupplier‘s implementation by caching the returned value to a private field:

public class LazyLambdaSupplier<T> extends LambdaSupplier<T> {
    private T data;
    public LazyLambdaSupplier(Supplier<T> expensiveData) {
        super(expensiveData);
    }
    @Override
    public T getData() {
        if (data != null) {
            return data;
        }
        return data = expensiveData.get();
    }
}

This implementation stores the returned value to the private field data so the value can be re-used in consecutive calls.

The following test case verifies that the new implementation doesn’t make multiple calls when called sequentially:

@Test
public void whenCalledMultipleTimes_thenShouldBeCalledOnlyOnce() {
    @SuppressWarnings("unchecked") Supplier<String> mockedExpensiveFunction = Mockito.mock(Supplier.class);
    Mockito.when(mockedExpensiveFunction.get())
        .thenReturn("expensive call");
    LazyLambdaSupplier<String> testee = new LazyLambdaSupplier<>(mockedExpensiveFunction);
    Mockito.verify(mockedExpensiveFunction, Mockito.never())
        .get();
    testee.getData();
    testee.getData();
    Mockito.verify(mockedExpensiveFunction, Mockito.times(1))
        .get();
}

Essentially, the template of this test case is the same as our previous test case. The important difference is that in the second case, we verify that the mocked function was called only once.

To show that this solution isn’t thread-safe, let’s write a test case with concurrent executions:

@Test
public void whenCalledMultipleTimesConcurrently_thenShouldBeCalledMultipleTimes() throws InterruptedException {
    @SuppressWarnings("unchecked") Supplier mockedExpensiveFunction = Mockito.mock(Supplier.class);
    Mockito.when(mockedExpensiveFunction.get())
        .thenAnswer((Answer) invocation -> {
            Thread.sleep(1000L);
            return "Late response!";
        });
    LazyLambdaSupplier testee = new LazyLambdaSupplier<>(mockedExpensiveFunction);
    Mockito.verify(mockedExpensiveFunction, Mockito.never())
        .get();
    ExecutorService executorService = Executors.newFixedThreadPool(4);
    executorService.invokeAll(List.of(testee::getData, testee::getData));
    executorService.shutdown();
    if (!executorService.awaitTermination(5, TimeUnit.SECONDS)) {
        executorService.shutdownNow();
    }
    Mockito.verify(mockedExpensiveFunction, Mockito.times(2))
        .get();
}

The Supplier.get() function is invoked twice in the above test. To make that happen, the ExecutorService simultaneously invokes two threads that call the LazyLambdaSupplier.getData() function. Furthermore, the Thread.sleep() call we added to the mockedExpensiveFunction guarantees that the field data will still be null when the getData() function is called by both threads.

4. Thread-Safe Solution

Finally, let’s tackle the thread safety limitation that we demonstrated above. To accomplish that, we’ll need to use synchronized data access and a thread-safe value wrapper, namely the AtomicReference.

Let’s combine what we’ve learned so far to write the LazyLambdaThreadSafeSupplier:

public class LazyLambdaThreadSafeSupplier<T> extends LambdaSupplier<T> {
    private final AtomicReference<T> data;
    public LazyLambdaThreadSafeSupplier(Supplier<T> expensiveData) {
        super(expensiveData);
        data = new AtomicReference<>();
    }
    public T getData() {
        if (data.get() == null) {
            synchronized (data) {
                if (data.get() == null) {
                    data.set(expensiveData.get());
                }
            }
        }
        return data.get();
    }
}

To explain why this approach is thread-safe, we need to imagine that multiple threads call the getData() method all at once. Threads will indeed block and the execution will be sequential until the data.get() call is not null. Once the data field initialization is complete, then multiple threads can access it concurrently.

At first glance, someone might argue that the double null check in the getData() method is redundant, but it’s not. In fact, the outer null check ensures that when the data.get() is not null, the threads do not block on the synchronized block.

To verify that our implementation is thread-safe, let’s provide a unit test in the same fashion as we did for the previous solutions:

@Test
public void whenCalledMultipleTimesConcurrently_thenShouldBeCalledOnlyOnce() throws InterruptedException {
    @SuppressWarnings("unchecked") Supplier mockedExpensiveFunction = Mockito.mock(Supplier.class);
    Mockito.when(mockedExpensiveFunction.get())
        .thenAnswer((Answer) invocation -> {
            Thread.sleep(1000L);
            return "Late response!";
        });
    LazyLambdaThreadSafeSupplier testee = new LazyLambdaThreadSafeSupplier<>(mockedExpensiveFunction);
    Mockito.verify(mockedExpensiveFunction, Mockito.never())
        .get();
    ExecutorService executorService = Executors.newFixedThreadPool(4);
    executorService.invokeAll(List.of(testee::getData, testee::getData));
    executorService.shutdown();
    if (!executorService.awaitTermination(5, TimeUnit.SECONDS)) {
        executorService.shutdownNow();
    }
    Mockito.verify(mockedExpensiveFunction, Mockito.times(1))
        .get();
}

5. Conclusion

In this article, we showed different ways to lazily initialize fields using lambda functions. By using this approach, we can avoid executing expensive calls more than once and also defer them. Our examples can be used as an alternative to local caches or Project Lombok‘s lazy-getter.

As always, the source code for our examples is available over on GitHub.

       

Reactor WebFlux vs Virtual Threads

$
0
0

1. Overview

In this tutorial, we’ll compare Java 19’s virtual threads with Project Reactor’s Webflux. We’ll begin by revisiting the fundamental workings of each approach, and subsequently, we’ll analyze their strengths and weaknesses.

We’ll start by exploring the strengths of reactive frameworks and we’ll see why WebFlux remains valuable. After that, we’ll discuss the thread-per-request approach and highlight scenarios where virtual threads can be a better option.

2. Code Examples

For the code examples in this article, we’ll assume we’re developing the backend of an e-commerce application. We’ll focus on the function responsible for computing and publishing the price of an item added to a shopping cart:

class ProductService {
    private final String PRODUCT_ADDED_TO_CART_TOPIC = "product-added-to-cart";
    private final ProductRepository repository;
    private final DiscountService discountService;
    private final KafkaTemplate<String, ProductAddedToCartEvent> kafkaTemplate;
    // constructor
    public void addProductToCart(String productId, String cartId) {
        Product product = repository.findById(productId)
          .orElseThrow(() -> new IllegalArgumentException("not found!"));
        Price price = product.basePrice();
        if (product.category().isEligibleForDiscount()) {
            BigDecimal discount = discountService.discountForProduct(productId);
            price.setValue(price.getValue().subtract(discount));
        }
        var event = new ProductAddedToCartEvent(productId, price.getValue(), price.getCurrency(), cartId);
        kafkaTemplate.send(PRODUCT_ADDED_TO_CART_TOPIC, cartId, event);
    }
}

As we can see, we begin by retrieving the Product from the MongoDB database using a MongoRepository. Once retrieved, we determine if the Product qualifies for discounts. If this is the case, we use DiscountService to perform an HTTP request to ascertain any available discounts for the product.

Finally, we calculate the final price for the product. Upon completion, we dispatch a Kafka message containing the productId, cartId, and the computed price.

3. WebFlux

WebFlux is a framework for building asynchronous, non-blocking, and event-driven applications. It operates on reactive programming principles, leveraging the Flux and Mono types to handle the intricacies of asynchronous communication. These types implement the publisher-subscriber design pattern, decoupling the consumer and the producer of the data.

3.1. Reactive Libraries

Numerous modules from the Spring ecosystem integrate with WebFlux for reactive programming. Let’s use some of these modules while refactoring our code toward a reactive paradigm.

For instance, we can switch the MongoRepository to a ReactiveMongoRepository. This change means we’ll have to work with a Mono<Product> instead of an Optional<Product>:

Mono<Product> product = repository.findById(productId)
  .switchIfEmpty(Mono.error(() -> new IllegalArgumentException("not found!")));

Similarly, we can change the ProductService to be asynchronous and non-blocking. For example, we can make it use WebClient for performing the HTTP requests, and, consequently, return the discount as a Mono<BigDecimal>:

Mono<BigDecimal> discount = discountService.discountForProduct(productId);

3.2. Immutability

In functional and reactive programming paradigms, immutability is always preferred over mutable data. Our initial method involves altering the Price‘s value using a setter. However, as we move towards a reactive approach, let’s refactor the Price object and make it immutable.

For example, we can introduce a dedicated method that applies the discount and generates a new Price instance rather than modifying the existing one:

record Price(BigDecimal value, String currency) {  
    public Price applyDiscount(BigDecimal discount) {
        return new Price(value.subtract(discount), currency);
    }
}

Now, we can compute the new price based on the discount, using WebFlux’s map() method:

Mono<Price> price = discountService.discountForProduct(productId)
  .map(discount -> price.applyDiscount(discount));

Additionally, we can even use a method reference here, to keep the code compact:

Mono<Price> price = discountService.discountForProduct(productId).map(price::applyDiscount);

3.3. Functional Pipelines

Mono and Flux adhere to the functor and monad patterns, through methods such as map() and flatMap(). This allows us to describe our use case as a pipeline of transformations on immutable data.

Let’s try to identify the transformations needed for our use case:

  • we start with a raw productId
  • we transform it into a Product
  • we use the Product to compute a Price
  • we use the Price to create an event
  • finally, we publish the event on a message queue

Now, let’s refactor the code to reflect this chain of functions:

void addProductToCart(String productId, String cartId) {
    Mono<Product> productMono = repository.findById(productId)
      .switchIfEmpty(Mono.error(() -> new IllegalArgumentException("not found!")));
    Mono<Price> priceMono = productMono.flatMap(product -> {
        if (product.category().isEligibleForDiscount()) {
            return discountService.discountForProduct(productId)
              .map(product.basePrice()::applyDiscount);
        }
        return Mono.just(product.basePrice());
    });
    Mono<ProductAddedToCartEvent> eventMono = priceMono.map(
      price -> new ProductAddedToCartEvent(productId, price.value(), price.currency(), cartId));
    eventMono.subscribe(event -> kafkaTemplate.send(PRODUCT_ADDED_TO_CART_TOPIC, cartId, event));
}

Now, let’s inline the local variables to keep the code compact. Additionally, let’s extract a function for computing the price, and use it inside of the flatMap():

void addProductToCart(String productId, String cartId) {
    repository.findById(productId)
      .switchIfEmpty(Mono.error(() -> new IllegalArgumentException("not found!")))
      .flatMap(this::computePrice)
      .map(price -> new ProductAddedToCartEvent(productId, price.value(), price.currency(), cartId))
      .subscribe(event -> kafkaTemplate.send(PRODUCT_ADDED_TO_CART_TOPIC, cartId, event));
}
Mono<Price> computePrice(Product product) {
    if (product.category().isEligibleForDiscount()) {
        return discountService.discountForProduct(product.id())
          .map(product.basePrice()::applyDiscount);
    }
    return Mono.just(product.basePrice());
}

4. Virtual Threads

Virtual Threads were introduced in Java via Project Loom as an alternative solution for parallel processing. They are lightweight, user-mode threads managed by the Java Virtual Machine (JVM). As a result, they are particularly well suited for I/O operations, where traditional threads may spend significant time waiting for external resources.

In contrast to asynchronous or reactive solutions, virtual threads enable us to keep using the thread-per-request processing model. In other words, we can keep writing code sequentially, without mixing the business logic and the reactive API.

4.1. Virtual Threads

There are several approaches available to utilize virtual threads for executing our code. For a single method, such as the one demonstrated in the previous example, we can employ startVirtualThread(). This static method was recently added to the Thread API and executes a Runnable on a new virtual thread:

public void addProductToCart(String productId, String cartId) {
    Thread.startVirtualThread(() -> computePriceAndPublishMessage(productId, cartId));
}
private void computePriceAndPublishMessage(String productId, String cartId) {
    // ...
}

Alternatively, we can create an ExecutorService that relies on virtual threads with the new static factory method Executors.newVirtualThreadPerTaskExecutor(). Furthermore, for applications using Spring Framework 6 and Spring Boot 3, we can leverage the new Executor and configure Spring to favor virtual threads over platform threads.

4.2. Compatibility

Virtual threads simplify code by using a more traditional synchronous programming model. As a result, we can write code in a sequential manner, akin to blocking I/O operations, without worrying about explicit reactive constructs.

Moreover, we can seamlessly switch from regular single-threaded code to virtual threads with minimal to no alterations. For instance, in our previous example, we simply need to create a virtual thread using the static factory method startVirtualThread() and execute logic inside of it:

void addProductToCart(String productId, String cartId) {
    Thread.startVirtualThread(() -> computePriceAndPublishMessage(productId, cartId));
}
void computePriceAndPublishMessage(String productId, String cartId) {
    Product product = repository.findById(productId)
      .orElseThrow(() -> new IllegalArgumentException("not found!"));
    Price price = computePrice(productId, product);
    var event = new ProductAddedToCartEvent(productId, price.value(), price.currency(), cartId);
    kafkaTemplate.send(PRODUCT_ADDED_TO_CART_TOPIC, cartId, event);
}
Price computePrice(String productId, Product product) {
    if (product.category().isEligibleForDiscount()) {
        BigDecimal discount = discountService.discountForProduct(productId);
        return product.basePrice().applyDiscount(discount);
    }
    return product.basePrice();
}

4.3. Readability

With the thread-per-request processing model, it can be easier to understand and reason about the business logic. This can reduce the cognitive load associated with reactive programming paradigms.

In other words, virtual threads allow us to cleanly separate the technical concerns from our business logic. As a result, it eliminates the need for external APIs in implementing our business use cases.

5. Conclusion

In this article, we compared two different approaches to concurrency and asynchronous processing. We started by analyzing the project Reactor’s WebFlux and the reactive programming paradigm. We discovered that this approach favors immutable objects and functional pipelines.

After that, we discussed virtual threads and their exceptional compatibility with legacy codebases that allow for a smooth transition to non-blocking code. Additionally, they have the added benefit of cleanly separating the business logic from the infrastructure code and other technical concerns.

As usual, all code samples used in this article are available over on GitHub.

       

What Does It Mean to Hydrate an Object?

$
0
0

1. Introduction

In this tutorial, we’ll talk about the term hydration in the context of programming and dive deep into what it means to hydrate a Java object. 

2. Object Hydration

2.1. Lazy Initialization

Lazy loading or lazy initialization of an object is a common pattern used in software applications. An object in Java is an instance of a class created using the new keyword. Objects are the building blocks of a program, and objects interact with other objects to achieve a desired functionality. 

Objects are generally meant to represent a real-world entity in object oriented programming paradigm, and hence, objects have several associated properties. Object initialization refers to the process of populating the properties of the object with real data. This is generally done by invoking a class constructor and passing in data as arguments. Initialization can also be done from a data source such as a network, database, or file system. 

Object initializations can, at times, be a resource-intensive operation, especially when the data is being fed from a different data source. Also, objects are not always used by the program immediately upon creation. 

In such scenarios, it is a good practice to defer the object initialization as long as possible until the point it is needed. This pattern is called lazy initialization, as we create the object with empty data at one time and lazily populate the object with relevant data in the future. A conscious delay of the data initialization helps boost code performance and memory utilization.

Let’s create a User class which has several attributes:

public class User {
    private String uId;
    private String firstName;
    private String lastName;
    private String alias;
    // constructor, getter-setters
}

We can create an object of User and keep it in memory without filling its attributes with meaningful data:

User iamUser = new User();

2.2. What is Hydration?

With lazy initialization, we deliberately delay the initialization process of an object that is already created and exists in memory. The process of populating data to an existing empty object is called hydration.

The User instance we created is a dummy instance. The object does not have any relevant data attributes, as it wasn’t required until this point. To make the object useful, we should fill the object with relevant domain data, which can be done by filling it with data from a source such as a network, database, or file system. 

We perform the hydration of the user instance in the following steps. We first write our hydration logic as a class-level method, which uses the class setters to set the relevant data. In our example, we’ll use our data. However, we can fetch data from a file or a similar source as well:

public void generateMyUser() {
    this.setAlias("007");
    this.setFirstName("James");
    this.setLastName("Bond");
    this.setuId("JB");
}

Now we create an empty instance of User, and when required, we hydrate the same instance by calling the generateMyUser() method:

User jamesBond = new User();
// performing hydration
jamesBond.generateMyUser();

We can validate the results of the hydration by asserting the state of its attributes:

User jamesBond = new User();
Assert.assertNull(jamesBond.getAlias());
jamesBond.generateMyUser();
Assert.assertEquals("007", jamesBond.getAlias());

3. Hydration and Deserialization

Hydration and Deserialization are not synonymous, and we should not use them interchangeably. Deserialization is a process used in programming to revive or recreate an object from its serialized form. We often store or transmit objects over a network. During the process, serialization (the conversion of the object to a stream of bytes) and deserialization (the reverse process of reviving the object) come in very handy. 

We can serialize our User instance into a file or equivalent:

try {
    FileOutputStream fileOut = new FileOutputStream(outputName);
    ObjectOutputStream out = new ObjectOutputStream(fileOut);
    out.writeObject(user);
    out.close();
    fileOut.close();
} catch (IOException e) {
    e.printStackTrace();
}

Similarly, when required, we can recreate the User instance from its serialized form:

try {
    FileInputStream fileIn = new FileInputStream(serialisedFile);
    ObjectInputStream in = new ObjectInputStream(fileIn);
    deserializedUser = (User) in.readObject();
    in.close();
    fileIn.close();
} catch (IOException | ClassNotFoundException e) {
    e.printStackTrace();
}

It is clear that hydration and deserialization both involve dealing with an object and filling it with data. However, the important difference between the two is that deserialization is mostly a single-step process of creating the instance and populating the attributes.

Hydration, on the other hand, is concerned with only adding the data to the attributes of a pre-formed object. Therefore, deserialization is object instantiation and object hydration in the same step. 

4. Hydration in ORM Frameworks

An ORM (Object Relational Mapping) framework is a software development paradigm that enables us to combine object-oriented programming with relational databases. ORM frameworks facilitate the mapping between the objects in the application code and the tables in a relational database and allow developers to interact with database entities as native objects. 

The idea of object hydration is more prevalent in ORM frameworks, such as Hibernate or JPA.

Let’s consider a JPA Entity class Book and its corresponding Repository class as follows:

@Entity
@Table(name = "books")
public class Book {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @Column(name = "name")
    private String name;
    // other columns and methods
}
public interface BookRepository extends JpaRepository<Book, Long> {
}

Based on ORM principles, the entity Book represents a table in our relational database. The entities’ interactions with the database are abstracted in the form of the BookRepository interface that we have defined above. An instance of the class represents a row in the table. 

When we load an instance of Book from the database by using one of the numerous inbuilt find() methods or using custom queries, the ORM framework performs several steps:

Book aTaleOfTwoCities = bookRepository.findOne(1L);

The framework initializes an empty object, typically by calling the default constructor of the class. Once the object is ready, the framework tries to load the attribute data from a cache store. If there is a cache miss at this point, the framework establishes a connection with the database and queries the table to fetch the row.

Once the ResultSet is obtained, the framework hydrates the aforementioned object aTaleOfTwoCities with the resultset object and finally returns the instance. 

5. Conclusion

In this article, we discussed the meaning of the term hydration in the context of programming. We saw how hydration differs from deserialization. Finally, we explored examples of object hydration in ORM frameworks and plain object models. 

As usual, the code for this article is available over on GitHub.

       

Convert Excel Files to PDF Using Java

$
0
0

1. Introduction

In this article, we’ll explore how to convert Excel files to PDF in Java using the Apache POI and iText libraries. Apache POI handles Excel file parsing and data extraction, while iText takes care of PDF document creation and formatting. By leveraging their strengths, we can efficiently convert Excel data while retaining its original formatting and styles.

2. Adding Dependencies

Before we start the implementation, we need to add the Apache POI and iText libraries to our project. In the pom.xml file, add the following dependencies:

<dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>poi-ooxml</artifactId>
</dependency>
<dependency>
    <groupId>com.itextpdf</groupId>
    <artifactId>itextpdf</artifactId>
</dependency>

The latest versions of the Apache POI and iText libraries can be downloaded from Maven Central.

3. Loading the Excel File

Load the Excel

With the libraries in place, let’s load the target Excel file using Apache POI. We’ll first open the Excel file using a FileInputStream and create an XSSFWorkbook object representing the loaded workbook:

FileInputStream inputStream = new FileInputStream(excelFilePath);
XSSFWorkbook workbook = new XSSFWorkbook(inputStream);

We’ll use this object to access individual sheets and their data.

4. Creating a PDF Document

Next, we’ll utilize iText to create a new PDF document:

Document document = new Document();
PdfWriter.getInstance(document, new FileOutputStream(pdfFilePath));
document.open();

This creates a new Document object and associates it with a PDFWriter instance responsible for writing the PDF content. Finally, we specify the desired output location for the PDF through a FileOutputStream.

5. Parsing the Excel Data

With the document ready, we’ll iterate through each row in the worksheet to extract the cell values:

void addTableData(PdfPTable table) throws DocumentException, IOException {
    XSSFSheet worksheet = workbook.getSheetAt(0);
    Iterator<Row> rowIterator = worksheet.iterator();
    while (rowIterator.hasNext()) {
        Row row = rowIterator.next();
        if (row.getRowNum() == 0) {
            continue;
        }
        for (int i = 0; i < row.getPhysicalNumberOfCells(); i++) {
            Cell cell = row.getCell(i);
            String cellValue;
            switch (cell.getCellType()) {
                case STRING:
                    cellValue = cell.getStringCellValue();
                    break;
                case NUMERIC:
                    cellValue = String.valueOf(BigDecimal.valueOf(cell.getNumericCellValue()));
                    break;
                case BLANK:
                default:
                    cellValue = "";
                    break;
            }
            PdfPCell cellPdf = new PdfPCell(new Phrase(cellValue));
            table.addCell(cellPdf);
        }
    }
}

The code first creates a PdfTable object matching the number of columns in the first row of the worksheet. Then, iterate through each row in the worksheet, extracting the cell values and weaving them into the PDF table. However, Excel formulas are currently not supported and will be returned with an empty string.

For each extracted cell value, we create a new PdfPCell object using a Phrase containing the extracted data. Phrase is an iText element that represents a formatted text string.

6. Preserving the Excel Styling

One of the key advantages of using Apache POI and iText is the ability to preserve the formatting and styles from the original Excel file. This includes font styles, colors, and alignments.

By accessing the relevant cell style information from Apache POI, we can apply it to the corresponding elements in the PDF document using iText. However, it’s important to note that this approach, while preserving formatting and styles, may not replicate the exact look and feel of a PDF directly exported from Excel or printed with a printer driver. For more complex formatting needs, additional adjustments may be required.

6.1. Font Styling

We’ll create a dedicated getCellStyle(Cell cell) method to extract styling information like font, color, etc., from the CellStyle object associated with each cell:

Font getCellStyle(Cell cell) throws DocumentException, IOException {
    Font font = new Font();
    CellStyle cellStyle = cell.getCellStyle();
    org.apache.poi.ss.usermodel.Font cellFont = cell.getSheet()
      .getWorkbook()
      .getFontAt(cellStyle.getFontIndexAsInt());
    if (cellFont.getItalic()) {
        font.setStyle(Font.ITALIC);
    }
    if (cellFont.getStrikeout()) {
        font.setStyle(Font.STRIKETHRU);
    }
    if (cellFont.getUnderline() == 1) {
        font.setStyle(Font.UNDERLINE);
    }
    short fontSize = cellFont.getFontHeightInPoints();
    font.setSize(fontSize);
    if (cellFont.getBold()) {
        font.setStyle(Font.BOLD);
    }
    String fontName = cellFont.getFontName();
    if (FontFactory.isRegistered(fontName)) {
        font.setFamily(fontName);
    } else {
        logger.warn("Unsupported font type: {}", fontName);
        font.setFamily("Helvetica");
    }
    return font;
}

The Phrase object can accept a cell value and a Front object as arguments to its constructor:

PdfPCell cellPdf = new PdfPCell(new Phrase(cellValue, getCellStyle(cell));

This allows us to control the content and formatting of text within a PDF cell. Note that iText’s built-in fonts are limited to Courier, Helvetica, and TimesRoman. Therefore, we should check if iText supports the extracted cell’s font family before we apply it directly. If the Excel file uses a different font family, it won’t be reflected in the PDF output.

6.2. Background Color Styling

In addition to preserving font styles, we also want to ensure that the background colors of cells in the Excel file are accurately reflected in the generated PDF. To achieve this, we’ll create a new method, setBackgroundColor(), to extract the background color information from the Excel cell and apply it to the corresponding PDF cell.

void setBackgroundColor(Cell cell, PdfPCell cellPdf) {
    short bgColorIndex = cell.getCellStyle()
      .getFillForegroundColor();
    if (bgColorIndex != IndexedColors.AUTOMATIC.getIndex()) {
        XSSFColor bgColor = (XSSFColor) cell.getCellStyle()
          .getFillForegroundColorColor();
        if (bgColor != null) {
            byte[] rgb = bgColor.getRGB();
            if (rgb != null && rgb.length == 3) {
                cellPdf.setBackgroundColor(new BaseColor(rgb[0] & 0xFF, rgb[1] & 0xFF, rgb[2] & 0xFF));
            }
        }
    }
}

6.3. Alignment Styling

Apache POI provides the getAlignment() method on the CellStyle object. This returns a constant value representing the alignment. Once we have the mapped iText alignment constant, we can set it on the PdfPCell object using the setHorizontalAlignment() method.

Here’s an example of how to incorporate alignment extraction and application:

void setCellAlignment(Cell cell, PdfPCell cellPdf) {
    CellStyle cellStyle = cell.getCellStyle();
    HorizontalAlignment horizontalAlignment = cellStyle.getAlignment();
    switch (horizontalAlignment) {
        case LEFT:
            cellPdf.setHorizontalAlignment(Element.ALIGN_LEFT);
            break;
        case CENTER:
            cellPdf.setHorizontalAlignment(Element.ALIGN_CENTER);
            break;
        case JUSTIFY:
        case FILL:
            cellPdf.setVerticalAlignment(Element.ALIGN_JUSTIFIED);
            break;
        case RIGHT:
            cellPdf.setHorizontalAlignment(Element.ALIGN_RIGHT);
            break;
    }
}

Now, let’s update the existing code where we iterate through the cells to include the font and background color styling:

PdfPCell cellPdf = new PdfPCell(new Phrase(cellValue, getCellStyle(cell)));
setBackgroundColor(cell, cellPdf);
setCellAlignment(cell, cellPdf);

Note that the resulting Excel will not look the same as a PDF exported from Excel (or a PDF print via a printer driver).

7. Saving the PDF Document

Finally, we can save the generated PDF document to the desired location. This involves closing the PDF document object and ensuring all resources are released properly:

document.add(table);
document.close();
workbook.close();Convert Excel to PDF

8. Conclusion

We’ve learned to convert Excel files to PDF in Java using Apache POI and iText. By combining the capabilities of Apache POI for Excel handling and iText for PDF generation, we can seamlessly preserve formatting and apply styles from Excel to PDF.

The code examples from this article can be found in the Maven-based project over on GitHub.

       

Find Files by Extension in Specified Directory in Java

$
0
0

1. Introduction

In this quick tutorial, we’ll see a few alternatives using core Java and external libraries to search for files in a directory (including sub-directories) that match a specific extension. We’ll go from simple arrays and lists to streams and other newer methods.

2. Setting up Our Filter

Since we need to filter files by extension, let’s start with a simple Predicate implementation. We’ll need a little input sanitization to ensure we match most use cases, like accepting extension names beginning with a dot or not:

public class MatchExtensionPredicate implements Predicate<Path> {
    private final String extension;
    public MatchExtensionPredicate(String extension) {
        if (!extension.startsWith(".")) {
            extension = "." + extension;
        }
        this.extension = extension.toLowerCase();
    }
    @Override
    public boolean test(Path path) {
        if (path == null) {
            return false;
        }
        return path.getFileName()
          .toString()
          .toLowerCase()
          .endsWith(extension);
    }
}

We start by writing our constructor, which prepends a dot before the extension name (if it doesn’t already contain one). Then, we transform it to lowercase. This way, when we compare it with other files, we can ensure they have the same case. Finally, we implement test() by getting the Path‘s file name and transforming it to lowercase. Most importantly, we check if it ends with the extension name we’re looking for.

3. Traversing Directories With Files.listFiles()

Our first example will use a method that’s been around since the dawn of Java: Files.listFiles(). Let’s start by instantiating a List to store our results and listing all files in the directory:

List<File> find(File startPath, String extension) {
    List<File> matches = new ArrayList<>();
    File[] files = startPath.listFiles();
    if (files == null) {
       return matches;
    }
    // ...
}

By itself, listFiles() doesn’t operate recursively, so for every item, if we identify it’s a directory, we start recursing:

MatchExtensionPredicate filter = new MatchExtensionPredicate(extension);
for (File file : files) {
    if (file.isDirectory()) {
        matches.addAll(find(file, extension));
    } else if (filter.test(file.toPath())) {
        matches.add(file);
    }
}
return matches;

We also instantiate our filter and only add the current file to our list if it passes our test() implementation. Ultimately, we’ll have all the results matching our filter. Note that this can cause a StackOverflowError in directory trees that are too deep and an OutOfMemoryError in directories that contain too many files. We’ll see options that perform better later.

4. Traversing Directories With Files.walkFileTree() From Java 7 Onwards

Starting with Java 7, we have the NIO2 APIs. It included many utilities like the Files class and a new way to handle files with the Path class. Using walkFileTree() allows us to traverse a directory recursively with zero effort. This method only requires a starting Path and a FileVisitor implementation:

List<Path> find(Path startPath, String extension) throws IOException {
    List<Path> matches = new ArrayList<>();
    Files.walkFileTree(startPath, new SimpleFileVisitor<Path>() {
        
        @Override
        public FileVisitResult visitFile(Path file, BasicFileAttributes attributes) {
            if (new MatchExtensionPredicate(extension).test(file)) {
                matches.add(file);
            }
            return FileVisitResult.CONTINUE;
        }
        @Override
        public FileVisitResult visitFileFailed(Path file, IOException exc) {
            return FileVisitResult.CONTINUE;
        }
    });
    return matches;
}

FileVisitor contains callbacks for a few events: before entering a directory, after leaving a directory, when visiting a file, and when this visit fails. But, with SimpleFileVisitor, we only need to implement the callbacks we’re interested in. In this case, it’s visiting a file with visitFile(). So, for every file visited, we test it against our Predicate and add it to a list of matching files.

Also, we’re implementing visitFileFailed() to always return FileVisitResult.CONTINUE. With this, we can continue searching for files even if an exception – like access denied – occurs.

5. Streaming With Files.walk() From Java 8 Onwards

Java 8 included a simpler way to traverse directories that integrate with the Stream API. Here’s how our method looks with Files.walk():

Stream<Path> find(Path startPath, String extension) throws IOException {
    return Files.walk(startPath)
      .filter(new MatchExtensionPredicate(extension));
}

Unfortunately, this breaks at the first exception thrown, and there’s no way to handle this yet. So, let’s try a different approach. We’ll start by implementing a FileVisitor that contains a Consumer<Path>. This time, we’ll use this Consumer to do whatever we want with our file matches instead of accumulating them in a List:

public class SimpleFileConsumerVisitor extends SimpleFileVisitor<Path> {
    private final Predicate<Path> filter;
    private final Consumer<Path> consumer;
    public SimpleFileConsumerVisitor(MatchExtensionPredicate filter, Consumer<Path> consumer) {
        this.filter = filter;
        this.consumer = consumer;
    }
    @Override
    public FileVisitResult visitFile(Path file, BasicFileAttributes attributes) {
        if (filter.test(file)) {
            consumer.accept(file);
        }
        return FileVisitResult.CONTINUE;
    }
    @Override
    public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException {
        return FileVisitResult.CONTINUE;
    }
}

Finally, let’s modify our find() method to use it:

void find(Path startPath, String extension, Consumer<Path> consumer) throws IOException {
    MatchExtensionPredicate filter = new MatchExtensionPredicate(extension);
    Files.walkFileTree(startPath, new SimpleFileConsumerVisitor(filter, consumer));
}

Note that we had to go back to Files.walkFileTree() to use our FileVisitor implementation.

6. Using Apache Commons IO’s FileUtils.iterateFiles()

Another helpful option is FileUtils.iterateFiles() from Apache Commons IO, which returns an Iterator. Let’s include its dependency:

<dependency>
    <groupId>commons-io</groupId>
    <artifactId>commons-io</artifactId>
    <version>2.13.0</version>
</dependency>

With its dependency, we can also use the WildcardFileFilter instead of our MatchExtensionPredicate:

Iterator<File> find(Path startPath, String extension) {
    if (!extension.startsWith(".")) {
        extension = "." + extension;
    }
    return FileUtils.iterateFiles(
      startPath.toFile(), 
      WildcardFileFilter.builder().setWildcards("*" + extension).get(), 
      TrueFileFilter.INSTANCE);
}

We start our method by ensuring the extension name is in the expected format. Checking if it’s necessary to prepend a dot allows our method to work if we pass “.extension” or just “extension.”

As with other methods, it just needs a starting directory. But, since this is an older API, it requires a File instead of a Path. The last argument is an optional directory filter. But, if not specified, it ignores subdirectories. So, we include a TrueFileFilter.INSTANCE to make sure the whole directory tree is visited.

7. Conclusion

In this article, we explored various approaches to searching for files in a directory and its subdirectories based on a specified extension. We started by setting up a flexible extension matching Predicate. Then, we covered different techniques, ranging from the traditional Files.listFiles() and Files.walkFileTree() methods to more modern alternatives introduced in Java 8, such as Files.walk(). Also, we explored the usage of Apache Commons IO’s FileUtils.iterateFiles() for a different perspective.

As always, the source code is available over on GitHub.

       

How to Get Last Record in Spring Data JPA

$
0
0

1. Overview

In this short tutorial, we’ll explore different ways of getting the last record in Spring Data JPA. First, we’ll see how to do it using the Derived Queries Methods. Then, we’ll explore how to achieve the same with the @Query annotation.

2. Setup

First, let’s create and initialize the table we want to query. Let’s start by creating a Post entity class:

@Entity
public class Post {
    @Id
    private Long id;
    private String title;
    private LocalDate publicationDate;
    // standard getters and setters
}

Here, @Entity indicates that the annotated class represents a table in the database. Similarly, the @Id annotation defines the primary key.

To keep things simple, we’ll be using H2 as our in-memory database. First, let’s add a basic SQL script to create the post table mapped to the Post class:

DROP TABLE IF EXISTS post;
CREATE TABLE post(
    id INT PRIMARY KEY,
    title VARCHAR(200),
    publication_date DATE
)

Next, let’s seed the table with data:

INSERT INTO post (id, title, publication_date) VALUES(1, 'Facebook post', '2020-11-10');
INSERT INTO post (id, title, publication_date) VALUES(2, 'Instagram post', '2020-12-24');
INSERT INTO post (id, title, publication_date) VALUES(3, 'Twitter post', '2023-01-10');
INSERT INTO post (id, title, publication_date) VALUES(4, 'tiktok post', '2023-03-18');
INSERT INTO post (id, title, publication_date) VALUES(5, 'Pinterest post', '2023-09-09');

As we can see, the last record here is the one with the id 5. So, to achieve our goal of getting the last record, we’ll reverse the order of the records based on publication_date. Then, we’ll use the Spring Data JPA methods to get the first record from the sorted result. That way, we can get the last record of the table.

3. Using Derived Query Methods

Spring Data JPA is praised for its derived query methods. This feature offers a convenient way to generate queries from method names without having to write SQL statements manually.

Spring Data JPA doesn’t provide any direct method to get the last record. On the other side, it offers straightforward ways to retrieve data from the start of a set of records.

For example, we can use the findFirst prefix to create a derived query that gets the first record. So, let’s see it in action:

public interface PostRepository extends JpaRepository<Post, Integer> {
    Post findFirstByOrderByPublicationDateDesc();
}

Each part of the method name findFirstByOrderByPublicationDateDesc() has its significance. The verb “find” tells Spring Data JPA to generate a select query, and “First” indicates that it should retrieve the first record from the result set.

Furthermore, “OrderByPublicationDateDesc” signifies that we want to sort the records in reverse order by the publicationDate property.

Here, Spring Data JPA evaluates the method name intelligently. It first sorts the posts in descending order by the publication date. That way, it puts the last record at the beginning of the result.

Then, it interprets “findFirst” to return the first element of the sorted records. As a result, we get the last record of the table.

Now, let’s add a test case to confirm that everything works as expected:

@Test
void givenPosts_whenUsingFindFirstDerivedQuery_thenReturnLastPost() {
    Post post = postRepository.findFirstByOrderByPublicationDateDesc();
    assertNotNull(post);
    assertEquals(5, post.getId());
}

We can see our test passing successfully.

Similarly, we can use the findTop keyword to accomplish the same outcome. We can use firstFirst or findTop interchangeably without any issue:

Post findTopByOrderByPublicationDateDesc();

Lastly, let’s create another test case for the findTopByOrderByPublicationDateDesc() method:

@Test
void givenPosts_whenUsingFindTopDerivedQuery_thenReturnLastPost() {
    Post post = postRepository.findTopByOrderByPublicationDateDesc();
    assertNotNull(post);
    assertEquals(5, post.getId());
}

As shown above, the test case passes with success.

4. Using @Query Annotation

Another solution would be using the @Query annotation to bind a method to a query that retrieves the last record. By default, @Query accepts JPQL queries. So, let’s add another method called findLastPost() to our PostRepository and use @Query to specify the query that gets the last post:

@Query("SELECT p FROM Post p ORDER BY p.publicationDate DESC LIMIT 1")
Post findLastPost();

In a nutshell, we selected the posts sorted in reverse order by publication date. Then, we used LIMIT 1 to retrieve only one post. The returned post denotes the last record.

As always, let’s add a test case to test our new method:

@Test
void givenPosts_whenUsingQueryAnnotation_thenReturnLastPost() {
    Post post = postRepository.findLastPost();
    assertNotNull(post);
    assertEquals(5, post.getId());
}

Unsurprisingly, the last record is the post with the id 5.

5. Conclusion

In this tutorial, we explored different ways of retrieving the last record of a specific table using Spring Data JPA. First, we saw how to achieve it using  Derived Queries Methods. Then, we wrote a JPQL query inside a @Query annotation, obtaining the same result.

As always, the complete code for this article can be found over on GitHub.

       

Java Weekly, Issue 521

$
0
0

1. Spring and Java

>> Stream API Evolution: A Closer Look at JEP 461’s Stream Gatherers [infoq.com]

Meet Stream Gatherers: helping us to keep using Stream API for more complex intermediate operations

>> JDK 21: The GCs keep getting better [kstefanj.github.io]

Java GCs keep getting better and better: shorter pauses, use less memory and have better throughput than ever before

>> Uniform handling of failure in switch [inside.java]

And, a proposal for throws cases: handling exceptions as yet another path based on evaluating the selector in switch expressions

Also worth reading:

Webinars and presentations:

Time to upgrade:

2. Technical & Musings

>> Five Apache projects you probably didn’t know about [blog.frankel.ch]

Some lesser-known Apache projects for use cases like real-time data warehouse, API gateway, sharding sphere, and more.

>> Bliki: SoftwareAndEngineering [martinfowler.com]

Is software engineering really an engineering discipline? An interesting discussion and pointers for this controversial topic.

Also worth reading:

3. Pick of the Week

>> You are never taught how to build quality software [florianbellmann.com]

       

Format LocalDate to ISO 8601 With T and Z

$
0
0

 1. Overview

Handling date and time in a standardized format is crucial when working with applications that deal with different time zones or when exchanging data between systems.

In this tutorial, we’ll explore various techniques for formatting a LocalDate to the ISO 8601 format. This format includes the ‘T‘ separator and the ‘Z‘ indicating UTC time.

2. LocalDate and ISO 8601

LocalDate is a part of the modern date and time API introduced in Java 8 under the java.time package. It’s immutable, meaning that once an instance is created, its value cannot be changed. It represents a date without considering the time or time zone, focusing solely on the year, month, and day of the month. LocalDate facilitates convenient manipulation and interaction with date information.

ISO 8601 is an international standard for representing dates and times in a clear, unambiguous, and universally accepted format. It provides a standardized way to express dates and times, which is essential for a wide range of applications. This includes data interchange, international communication, and computer systems.

The ISO 8601 format includes several components, with the most common format being: YYYY-MM-DDThh:mm:ss.sssZ.

Here’s a breakdown of the components:

  • YYYY: Represents the year with four digits (e.g., 2023)
  • MM: Represents the month with two digits (e.g., 03 for March)
  • DD: Represents the day of the month with two digits (e.g., 15)
  • T‘: A literal ‘T’ character that separates the date from the time
  • hh: Represents the hour of the day in 24-hour format (e.g., 14 for 2 PM)
  • mm: Represents the minutes (e.g., 30)
  • ss: Represents the seconds (e.g., 45)
  • sss: Represents milliseconds (optional and may vary in length)
  • Z‘: A literal ‘Z’ character that indicates the time is in Coordinated Universal Time (UTC)

ISO 8601 allows for various optional components, making it a versatile standard for representing date and time information. For example, we can include time zone offsets or omit seconds and milliseconds when they aren’t relevant.

The ‘Z’ character indicates that the time is in UTC, but we can also represent time in local time zones by specifying the offset from UTC.

3. Using Java 8 Time API

Java provides a flexible way to format date and time objects, including LocalDate using the DateTimeFormatter class.

Instances of DateTimeFormatter are thread-safe, making them suitable for use in multi-threaded environments without the need for external synchronization.

Here’s how we can use it to format a LocalDate to ISO 8601:

class LocalDateToISO {
    String formatUsingDateTimeFormatter(LocalDate localDate) {
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSX");
        String formattedDate = localDate.atStartOfDay().atOffset(ZoneOffset.UTC).format(formatter);
        return formattedDate;
    }

In this example, we create a DateTimeFormatter with a custom pattern that includes ‘T‘ and ‘Z’ in the desired positions. Then, we use the format() method to format the LocalDate into a string with the specified format.

We can perform a test to verify its expected behavior:

@Test
void givenLocalDate_whenUsingDateTimeFormatter_thenISOFormat(){
    LocalDateToISO localDateToISO = new LocalDateToISO();
    LocalDate localDate = LocalDate.of(2023, 11, 6);
    String expected = "2023-11-06T00:00:00.000Z";
    String actual = localDateToISO.formatUsingDateTimeFormatter(localDate);
    assertEquals(expected, actual);
}

4. Using SimpleDateFormat

The SimpleDateFormat class is a powerful tool for formatting and parsing dates. It belongs to the java.text package and provides a straightforward way to convert dates between their textual representations and Date objects.

It’s particularly useful for working with legacy date-time types, such as java.util.Date. While it’s not as modern or robust as the java.time API, it can still serve this purpose:

String formatUsingSimpleDateFormat(LocalDate date) {
    Date utilDate = Date.from(date.atStartOfDay(ZoneOffset.UTC).toInstant());
    DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSX");
    String formattedDate = dateFormat.format(utilDate);
    return formattedDate;
}

In the above example, we’re converting a LocalDate to a ZonedDateTime with ZoneOffset.UTC and then converts it to an Instant object. We can then obtain a Date object from an Instant and perform formatting on the object.

Let’s format a LocalDate object using SimpleDateFormat:

@Test
void givenLocalDate_whenUsingSimpleDateFormat_thenISOFormat(){
    LocalDateToISO localDateToISO = new LocalDateToISO();
    LocalDate localDate = LocalDate.of(2023, 11, 6);
    String expected = "2023-11-06T00:00:00.000Z";
    String actual = localDateToISO.formatUsingSimpleDateFormat(localDate);
    assertEquals(expected, actual);
}

It’s crucial to be aware that SimpleDateFormat isn’t thread-safe. Concurrent usage by multiple threads can lead to unexpected results or exceptions. To address this concern, developers often use ThreadLocal ensuring that each thread possesses its dedicated instance of SimpleDateFormat. This helps in effectively preventing potential thread-safety issues.

5. Using Apache Commons Lang3

The Apache Commons Lang3 library provides a utility class named FastDateFormat that simplifies date formatting. It’s a fast and thread-safe version of SimpleDateFormat. We can directly substitute this class for SimpleDateFormat in the majority of the formatting and parsing scenarios. It proves particularly beneficial in multi-threaded server environments.

This approach emphasizes conciseness by utilizing the features of Apache Commons Lang 3 to create Java date formatting code that is clear and straightforward to understand.

We can easily obtain the library from the central Maven repository by including the following dependency:

<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
    <version>3.12.0</version>
</dependency>

After installing the library, we can employ its methods. Here’s an example illustrating how to use it:

String formatUsingApacheCommonsLang(LocalDate localDate) {
    Date date = Date.from(localDate.atStartOfDay().toInstant(ZoneOffset.UTC));
    String formattedDate = FastDateFormat.getInstance("yyyy-MM-dd'T'HH:mm:ss.sss'Z'", TimeZone.getTimeZone("UTC"))
      .format(date);
    return formattedDate;
}

The above code example takes a LocalDate, converts it to a Date, and then formats it into a string with a specific pattern using FastDateFormat to format the LocalDate to ISO 8601.

Let’s move on to testing this example:

@Test
void givenLocalDate_whenUsingApacheCommonsLang_thenISOFormat() {
    LocalDateToISO localDateToISO = new LocalDateToISO();
    LocalDate localDate = LocalDate.of(2023, 11, 6);
    String expected = "2023-11-06T00:00:00.000Z";
    String actual = localDateToISO.formatUsingApacheCommonsLang(localDate);
    assertEquals(expected, actual);
}

6. Using Joda-Time

Joda-Time is a widely-used Java library designed to address the shortcomings of the original date and time classes in java.util package. Before the advent of the java.time API in Java 8, Joda-Time served as a popular and powerful alternative for handling date and time operations.

To incorporate the features of the Joda-Time library, we should include the following dependency to our pom.xml:

<dependency>
    <groupId>joda-time</groupId>
    <artifactId>joda-time</artifactId>
    <version>2.12.5</version>
</dependency>

While it’s no longer necessary in Java 8 and later, it remains an option for pre-existing codebases:

String formatUsingJodaTime(org.joda.time.LocalDate localDate) {
    org.joda.time.format.DateTimeFormatter formatter = ISODateTimeFormat.dateTime();
    return formatter.print(localDate.toDateTimeAtStartOfDay(DateTimeZone.UTC));
}

In the above example, the DateTimeFormatter from Joda-Time is used to format the LocalDate to ISO 8601.

Let’s test it out:

@Test
void givenLocalDate_whenUsingJodaTime_thenISOFormat() {
    LocalDateToISO localDateToISO = new LocalDateToISO();
    org.joda.time.LocalDate localDate = new org.joda.time.LocalDate(2023, 11, 6);
    String expected = "2023-11-06T00:00:00.000Z";
    String actual = localDateToISO.formatUsingJodaTime(localDate);
    assertEquals(expected, actual);
}

7. Conclusion

In this article, we talked about the different ways of formatting a LocalDate to ISO 8601 with ‘T‘ and ‘Z‘ in Java. The choice of method depends on our preference for code readability and maintainability.

We can choose the method that best suits our needs, and ensure that our date and time representations adhere to the ISO 8601 standard for consistency and interoperability. The DateTimeFormatter approach is more flexible and suitable for handling various formatting requirements, while the other methods provide simpler solutions for specific scenarios.

As always, the full source code is available over on GitHub.

       

Comparing One String With Multiple Values in One Expression in Java

$
0
0

1. Overview

In this tutorial, we’ll discuss the various ways of finding a string among a group of strings using a single expression.

Let’s assume there’s a fruit “Apple” and a group of fruits “Mango”, “Papaya”, “Apple”, “Pineapple”, etc. Now we’ll explore the various ways to see if the string “Apple” is present among the group of fruits.

2. Introduction to the Problem

Before we move on to the next sections covering the single expression solution, let’s look at an implementation with the if condition:

boolean compareWithMultipleStringsUsingIf(String str, String ... strs) {
    for(String s : strs) {
        if (str.equals(s)) {
            return true;
        }
    }
    return false;
}

This is a very basic implementation, perhaps the most popular among all the implementations. We iterate through the array of Strings and return true when the String str matches with any one of the elements in strs.

Let’s see how this works:

@Test
void givenStrings_whenCompareWithMultipleStringsUsingIf_thenSuccess() {
    String presentString = "Apple";
    String notPresentString = "Avocado";
    assertTrue(compareWithMultipleStringsUsingIf(presentString, "Mango", "Papaya", "Pineapple", "Apple"));
    assertFalse(compareWithMultipleStringsUsingIf(notPresentString, "Mango", "Papaya", "Pineapple", "Apple"));
}

We’re finding the presence of the Strings such as “Apple” and “Avacado” in a group of fruits. But this involves multiple lines of code. Hence let’s look at the upcoming sections for solutions involving a single expression.

3. Match With Set

java.util.Set has the contains() method which checks if an element exists in the collection. Hence, we’ll use java.util.Set for our use case with a single expression:

boolean compareWithMultipleStringsUsingSet(String str, String ... strs) {
    return Set.of(strs).contains(str);
}

With a single expression, we’ve initialized a Set and then used the contains() method to see if str is present in the Set. However, we cannot implement a single expression case-insensitive matching method using Set.

Let’s test the method compareWithMultipleStringsUsingSet():

@Test
void givenStrings_whenCompareWithMultipleStringsUsingSet_thenSuccess() {
    String presentString = "Apple";
    String notPresentString = "Avocado";
    assertTrue(compareWithMultipleStringsUsingSet(presentString, "Mango", "Papaya", "Pineapple", "Apple"));
    assertFalse(compareWithMultipleStringsUsingSet(notPresentString, "Mango", "Papaya", "Pineapple", "Apple"));
}

Just like before, we first pass “Apple” to the method, and it returns true and when we pass “Avocado”, it returns false. Hence, that works pretty well.

4. Match With List

Similar to Set, List also has the contains() method. Hence let’s check out the implementation using List:

boolean compareWithMultipleStringsUsingList(String str, String ... strs) {
    return List.of(strs).contains(str);
}

There isn’t much difference, we’ve replaced Set with List.

Let’s see it in action as well:

@Test
void givenStrings_whenCompareWithMultipleStringsUsingList_thenSuccess() {
    String presentString = "Apple";
    String notPresentString = "Avocado";
    assertTrue(compareWithMultipleStringsUsingList(presentString, "Mango", "Papaya", "Pineapple", "Apple"));
    assertFalse(compareWithMultipleStringsUsingList(notPresentString, "Mango", "Papaya", "Pineapple", "Apple"));
}

The contains() method works as expected and it returns the correct result.

5. Match With Stream

The Stream API encourages the use of declarative statements and hence it can help us in implementing a single expression method:

boolean compareWithMultipleStringsUsingStream(String str, String ... strs) {
    return Arrays.stream(strs).anyMatch(str::equals);
}

In a single expression, we converted the array of Strings into a Stream, and then we used the method anyMatch(). The method anyMatch() is a terminal operation in a Stream pipeline. Each element in the Stream is compared with the String str. However, the anyMatch() method returns the first match.

Let’s check out the method in action:

@Test
void givenStrings_whenCompareWithMultipleStringsUsingStream_thenSuccess() {
    String presentString = "Apple";
    String notPresentString = "Avocado";
    assertTrue(compareWithMultipleStringsUsingStream(presentString, "Mango", "Papaya", "Pineapple", "Apple"));
    assertFalse(compareWithMultipleStringsUsingStream(notPresentString, "Mango", "Papaya", "Pineapple", "Apple"));
}

The method works as expected with “Apple” and “Avocado” as the first argument, returning true and false respectively.

Let’s see a case-insensitive version using Stream:

boolean compareCaseInsensitiveWithMultipleStringsUsingStream(String str, String ... strs) {
    return Arrays.stream(strs).anyMatch(str::equalsIgnoreCase);
}

Unlike the earlier version we had to call the equalsIgnoreCase() method as a predicate to anyMatch().

We can now take a look at the method in action:

@Test
void givenStrings_whenCompareCaseInsensitiveWithMultipleStringsUsingStream_thenSuccess() {
    String presentString = "APPLE";
    String notPresentString = "AVOCADO";
    assertTrue(compareCaseInsensitiveWithMultipleStringsUsingStream(presentString, "Mango", "Papaya", "Pineapple", "Apple"));
    assertFalse(compareCaseInsensitiveWithMultipleStringsUsingStream(notPresentString, "Mango", "Papaya", "Pineapple", "Apple"));
}

This time it can find out “APPLE” among “Mango”, “Papaya”, “Pineapple”, and “Apple”.

6. Match With StringUtils

Before we can use the class StringUtils from the commons-lang3 library, let’s first update the pom.xml with its Maven dependency:

<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
    <version>3.13.0</version>
</dependency>

Now, let’s use the class StringUtils:

boolean compareWithMultipleStringsUsingStringUtils(String str, String ... strs) {
    return StringUtils.equalsAny(str, strs);
}

The method equalsAny() in StringUtils helps implement our use case in a single expression.

Let’s see the method in action:

@Test
void givenStrings_whenCompareWithMultipleStringsUsingStringUtils_thenSuccess() {
    String presentString = "Apple";
    String notPresentString = "Avocado";
    assertTrue(compareWithMultipleStringsUsingStringUtils(presentString, "Mango", "Papaya", "Pineapple", "Apple"));
    assertFalse(compareWithMultipleStringsUsingStringUtils(notPresentString, "Mango", "Papaya", "Pineapple", "Apple"));
}

This also works as expected, returning true and false when “Apple” and “Mango” are passed to the method, respectively.

Let’s take a look at the case-insensitive version of the method as well:

boolean compareCaseInsensitiveWithMultipleStringsUsingStringUtils(String str, String ... strs) {
    return StringUtils.equalsAnyIgnoreCase(str, strs);
}

Instead of the method equalsAny() we used the method equalsAnyIgnoreCase() of StringUtils.

Let’s see how the method works:

@Test
void givenStrings_whenCompareCaseInsensitiveWithMultipleStringsUsingStringUtils_thenSuccess() {
    String presentString = "APPLE";
    String notPresentString = "AVOCADO";
    assertTrue(compareCaseInsensitiveWithMultipleStringsUsingStringUtils(presentString, "Mango", "Papaya", "Pineapple", "Apple"));
    assertFalse(compareCaseInsensitiveWithMultipleStringsUsingStringUtils(notPresentString, "Mango", "Papaya", "Pineapple", "Apple"));
}

We could successfully find the String “APPLE” among the group of fruits.

7. Match With ArrayUtils

Furthermore, we’ll see another class ArrayUtils from the same commons-lang library:

boolean compareWithMultipleStringsUsingArrayUtils(String str, String ... strs) {
    return ArrayUtils.contains(strs, str);
}

ArrayUtils is a utility class that helps to check if an element is present in an array of objects. Hence, we exploited it to help us implement our use case involving Strings. Unfortunately, ArrayUtils doesn’t provide any method to find a String object in a case-insensitive way. Hence we cannot do a single expression implementation for the same.

Let’s see how the method compareWithAnyUsingArrayUtils() works:

@Test
void givenStrings_whenCompareWithMultipleStringsUsingArrayUtils_thenSuccess() {
    String presentString = "Apple";
    String notPresentString = "Avocado";
    assertTrue(compareWithMultipleStringsUsingArrayUtils(presentString, "Mango", "Papaya", "Pineapple", "Apple"));
    assertFalse(compareWithMultipleStringsUsingArrayUtils(notPresentString, "Mango", "Papaya", "Pineapple", "Apple"));
}

Quite unsurprisingly, it works as well.

8. Match With Regular Expression

Regular expression helps find patterns and hence we’ll use it for our use case:

boolean compareWithMultipleStringsUsingRegularExpression(String str, String ... strs) {
    return str.matches(String.join("|", strs));
}

The method String.join() creates a pipe-delimited list of Strings, for example, Mango|Papaya|Pineapple|Apple which is used as a regular expression pattern. In a single expression, we created the regular expression pattern with the array of Strings and then used the method matches() to check if the String str has the pattern.

Time to see the method in action:

@Test
void givenStrings_whenCompareWithMultipleStringsUsingRegularExpression_thenSuccess() {
    String presentString = "Apple";
    String notPresentString = "Avocado";
    assertTrue(compareWithMultipleStringsUsingRegularExpression(presentString, "Mango", "Papaya", "Pineapple", "Apple"));
    assertFalse(compareWithMultipleStringsUsingRegularExpression(notPresentString, "Mango", "Papaya", "Pineapple", "Apple"));
}

The method returns true for the argument “Mango” and false for “Avocado”. Hence, we can say it works too. However, regular expressions are always performance-intensive, so it’s better to avoid them.

Now, let’s take a look at the case-insensitive implementation:

boolean compareCaseInsensitiveWithMultipleStringsUsingRegularExpression(String str, String ... strs) {
    return str.matches("(?i)" + String.join("|", strs));
}

We just had to modify the regular expression by prepending it with (?i) to do case-insensitive pattern matching.

Let’s take a look at the method in action:

@Test
void givenStrings_whenCompareCaseInsensitiveUsingRegularExpression_thenSuccess() {
    String presentString = "APPLE";
    String notPresentString = "AVOCADO";
    assertTrue(compareCaseInsensitiveWithMultipleStringsUsingRegularExpression(presentString, "Mango", "Papaya", "Pineapple", "Apple"));
    assertFalse(compareCaseInsensitiveWithMultipleStringsUsingRegularExpression(notPresentString, "Mango", "Papaya", "Pineapple", "Apple"));
}

By using the method now we can find “APPLE” in the provided group of fruits as well.

9. Benchmark

Let’s calculate the average execution time of each of the single expression methods using Java Microbenchmark Harness (JMH).

Let’s take a look at the class configured for carrying out the benchmark:

@State(Scope.Benchmark)
@BenchmarkMode(Mode.AverageTime)
@Warmup(iterations = 2)
@Measurement(iterations = 5)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Fork(value = 1)
public class CompareAnyBenchmark {
    private final String[] groupOfFruits = {"Apple", "Mango", "Dragon Fruit", "Water Melon", "Avocado", "Guava", "Orange"};
    private final String fruit = "Apple";
    @Benchmark
    public boolean compareWithMultipleStringsUsingStringUtils() {
        return StringUtils.equalsAny(fruit, groupOfFruits);
    }
    @Benchmark
    public boolean compareCaseInsensitiveWithMultipleStringsUsingStringUtils() {
        return StringUtils.equalsAnyIgnoreCase(fruit, groupOfFruits);
    }
    //Other benchmark methods...
    public static void main(String[] args) throws Exception {
        Options options = new OptionsBuilder().include(CompareAnyBenchmark.class.getSimpleName())
          .threads(1)
          .shouldFailOnError(true)
          .shouldDoGC(true)
          .jvmArgs("-server")
          .build();
        new Runner(options).run();
    }
}

The annotations at the class level set up the benchmark to measure the average time, in nanoseconds, taken for five runs of each method. Finally, the main() method is for running the benchmark.

Let’s now take a look at the average execution time taken for each of the methods we discussed so far:

Method Name Avg. Time Error(±) Unit
compareWithMultipleStringsUsingArrayUtils() 1.150 0.031 ns/op
compareWithMultipleStringsUsingRegularExpression() 1175.809 177.940 ns/op
compareWithMultipleStringsUsingSet() 96.961 11.943 ns/op
compareWithMultipleStringsUsingList()
28.718 1.612 ns/op
compareWithMultipleStringsUsingStream() 47.266 3.968 ns/op
compareWithMultipleStringsUsingStringUtils 1.507 0.040 ns/op
compareCaseInsensitiveWithMultipleStringsUsingRegularExpression() 1803.497 645.104 ns/op
compareCaseInsensitiveWithMultipleStringsUsingStream() 63.079 56.509 ns/op
compareCaseInsensitiveWithMultipleStringsUsingStringUtils() 1.521 0.077 ns/op

The methods compareCaseInsensitiveWithMultipleStringsUsingRegularExpression() and compareWithMultipleStringsUsingRegularExpression() using regular expressions take the longest to execute. On the other hand, the methods compareWithMultipleStringsUsingArrayUtils(), and compareWithMultipleStringsUsingStringUtils() take the least time to execute.

Without considering any external library, the methods compareWithMultipleStringsUsingStream(), and compareCaseInsensitiveWithMultipleStringsUsingStream() have the best score. Moreover, the performance also doesn’t vary much for case-insensitive searches.

10. Conclusion

In this article, we explored various ways to find the presence of a String in a group of Strings. With java.util.Set, java.util.List, java.util.Stream and regular expressions we didn’t use any external library outside of the JDK. Hence it’s advisable to use them rather than going for external libraries like commons-lang. Moreover, the List implementation is the best choice in the JDK library.

As usual, the code examples can be found over on GitHub.

       

Read a File and Split it Into Multiple Files in Java

$
0
0

1. Overview

In this tutorial, we’ll learn how to split a large file in Java. First, we’ll compare reading files in memory with reading files using streams. Later, we’ll learn to split files based on their size and number.

2. Read File In-Memory vs. Stream

Whenever we read files in memory, the JVM keeps all the lines in memory. This is a good choice for small files. For large files, however, it frequently results in an OutOfMemoryException.

Streaming through a file is another way to read it, and there are many ways to stream and read large files. Because the whole file isn’t in memory, it consumes less memory and works well with large files without throwing an exception.

For our examples, we’ll use streams to read the large files.

3. File Split by File Size

While we’ve learned to read large files so far, sometimes we need to split them into smaller files or send them over the network in smaller sizes.
First, we’ll begin by splitting the large file into smaller files, each with a specific size.
For our example, we’ll take one 4.3MB file, largeFile.txt, in our project src/main/resource folder and split it into 1MB each files, and store them under the /target/split directory.
Let’s first get the large file and open an input stream on it:

File largeFile = new File("LARGE_FILE_PATH");
InputStream inputstream = Files.newInputStream(largeFile.toPath());

Here, we’re just loading the file metadata, the large file content isn’t loaded into memory yet.

For our example, we’ve got a constant fixed size. In practical use cases, this maxSizeOfSplitFiles value can be dynamically read and changed as per application need.

Now, let’s have a method that takes the largeFile object and a defined maxSizeOfSplitFiles for the split file:

public List<File> splitByFileSize(File largeFile, int maxSizeOfSplitFiles<em>,</em> String splitFileDirPath) 
  throws IOException {
    // ...
}

Now, let’s create a class SplitLargeFile and splitByFileSize() method:

class SplitLargeFile {
    public List<File> splitByFileSize(File largeFile, int maxSizeOfSplitFiles, String splitFileDirPath) 
      throws IOException {
        List<File> listOfSplitFiles = new ArrayList<>();
        try (InputStream in = Files.newInputStream(largeFile.toPath())) {
            final byte[] buffer = new byte[maxSizeOfSplitFiles];
            int dataRead = in.read(buffer);
            while (dataRead > -1) {
                File splitFile = getSplitFile(FilenameUtils.removeExtension(largeFile.getName()),
                  buffer, dataRead, splitFileDirPath);
                listOfSplitFiles.add(splitFile);
                dataRead = in.read(buffer);
            }
        }
        return listOfSplitFiles;
    }
    private File getSplitFile(String largeFileName, byte[] buffer, int length, String splitFileDirPath) 
      throws IOException {
        File splitFile = File.createTempFile(largeFileName + "-", "-split", new File(splitFileDirPath));
        try (FileOutputStream fos = new FileOutputStream(splitFile)) {
            fos.write(buffer, 0, length);
        }
        return splitFile;
    }
}

Using maxSizeOfSplitFiles, we can specify how many bytes each smaller chunked file can be.
The maxSizeOfSplitFiles amount of data will be loaded into memory, processed, and made into a small file. We then get rid of it. We read the next set of maxSizeOfSplitFiles data. This ensures that no OutOfMemoryException is thrown.
As a final step, the method returns a list of split files stored under the splitFileDirPath.
We can store the split file in any temporary directory or any custom directory.
Now, let’s test this:

public class SplitLargeFileUnitTest {
    @BeforeClass
    static void prepareData() throws IOException {
        Files.createDirectories(Paths.get("target/split"));
    }
    private String splitFileDirPath() throws Exception {
        return Paths.get("target").toString() + "/split";
    }
    private Path largeFilePath() throws Exception {
        return Paths.get(this.getClass().getClassLoader().getResource("largeFile.txt").toURI());
    }
    @Test
    void givenLargeFile_whenSplitLargeFile_thenSplitBySize() throws Exception {
        File input = largeFilePath().toFile();
        SplitLargeFile slf = new SplitLargeFile();
        slf.splitByFileSize(input, 1024_000, splitFileDirPath());
    }
}

Finally, once we test, we can see that the program splits the large file into four files of 1MB and one file of 240KB and puts them under the project target/split directory.

4. File Split by File Count

Now, let’s split the given large file into a specified number of smaller files. For this, first, we’ll check if the size of small files will fit or not as per the number of files counted.

Also, we’ll use the same method splitByFileSize() from earlier internally for the actual splitting.

Let’s create a method splitByNumberOfFiles():

class SplitLargeFile {
    public List<File> splitByNumberOfFiles(File largeFile, int noOfFiles, String splitFileDirPath)
      throws IOException {
        return splitByFileSize(largeFile, getSizeInBytes(largeFile.length(), noOfFiles), splitFileDirPath);
    }
    private int getSizeInBytes(long largefileSizeInBytes, int numberOfFilesforSplit) {
        if (largefileSizeInBytes % numberOfFilesforSplit != 0) {
            largefileSizeInBytes = ((largefileSizeInBytes / numberOfFilesforSplit) + 1) * numberOfFilesforSplit;
        }
        long x = largefileSizeInBytes / numberOfFilesforSplit;
        if (x > Integer.MAX_VALUE) {
            throw new NumberFormatException("size too large");
        }
        return (int) x;
    }
}

Now, let’s test this:

@Test
void givenLargeFile_whenSplitLargeFile_thenSplitByNumberOfFiles() throws Exception { 
    File input = largeFilePath().toFile(); 
    SplitLargeFile slf = new SplitLargeFile(); 
    slf.splitByNumberOfFiles(input, 3, splitFileDirPath()); 
}

Finally, once we test, we can see that the program splits the large file into 3 files of 1.4MB and puts it under the project target/split dir.

5. Conclusion

In this article, we saw the differences between reading files in memory and via stream, which helps us choose the appropriate one for any use case. Later, we discussed how to split large files into small files. We then learned about splitting by size and splitting by number of files.

As always, the example code used in this article is over on GitHub.

       

Calculate Months Between Two Dates in Java

$
0
0

1. Overview

Calculating month intervals between dates is a common programming task. The Java standard library and third-party libraries provide classes and methods to calculate months between two dates.

In this tutorial, we’ll delve into details of how to use the legacy Date API, Date Time API, and Joda-Time library to calculate month intervals between two dates in Java.

2. Impact of Day Value

When calculating the month interval between two dates, the day value of the dates impacts the result.

By default, the Java standard library and Joda-Time library consider the day value. If the day value of the end date is less than the day value of the start day, the final month isn’t counted as a full month and is excluded from the month interval:

LocalDate startDate = LocalDate.parse("2023-05-31");
LocalDate endDate = LocalDate.parse("2023-11-28");

In the code above, the day value of the end date is less than the day value of the start date. Therefore, the final month won’t count as a full month.

However, if the day value of the end date is equal to or greater than the day value of the day value of the start date, the final month is considered a full month and included in the interval:

LocalDate startDate = LocalDate.parse("2023-05-15");
LocalDate endDate = LocalDate.parse("2023-11-20");

In some cases, we may decide to ignore the day value completely and only consider the month value difference between two dates.

In the subsequent sections, we’ll see how to calculate month intervals between two dates with or without considering the day value.

3. Using Legacy Date API

When using the legacy Date API, we need to create a custom method to calculate month intervals between two dates. With the Calendar class, we can calculate the month interval between two dates using the Date class.

3.1. Without the Day Value

Let’s write a method that uses the Calendar and Date classes to calculate the month interval between two Date objects without considering the day value:

int monthsBetween(Date startDate, Date endDate) {
    if (startDate == null || endDate == null) {
        throw new IllegalArgumentException("Both startDate and endDate must be provided");
    }
    Calendar startCalendar = Calendar.getInstance();
    startCalendar.setTime(startDate);
    int startDateTotalMonths = 12 * startCalendar.get(Calendar.YEAR) 
      + startCalendar.get(Calendar.MONTH);
    Calendar endCalendar = Calendar.getInstance();
    endCalendar.setTime(endDate);
    int endDateTotalMonths = 12 * endCalendar.get(Calendar.YEAR) 
      + endCalendar.get(Calendar.MONTH);
    return endDateTotalMonths - startDateTotalMonths;
}

This method takes the start date and end date as arguments. First, we create a Calendar instance and pass the Date object to it. Next, we calculate the total months of each date by converting the year to month and adding it to the current month value.

Finally, we find the difference between the two dates.

Let’s write a unit test for the method:

@Test
void whenCalculatingMonthsBetweenUsingLegacyDateApi_thenReturnMonthsDifference() throws ParseException {
    MonthInterval monthDifference = new MonthInterval();
    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
    
    Date startDate = sdf.parse("2023-05-31");
    Date endDate = sdf.parse("2016-11-30");
    int monthsBetween = monthDifference.monthsBetween(startDate, endDate);
    
    assertEquals(6, monthsBetween);
}

In the code above, we create a SimpleDateFormat object to format the date. Next, we pass the Date object to the monthsBetween() to calculate the month interval.

Finally, we assert that the output is equal to the expected result.

3.2. With the Day Value

Also, let’s see an example code that puts the day value into consideration:

int monthsBetweenWithDayValue(Date startDate, Date endDate) {
    if (startDate == null || endDate == null) {
        throw new IllegalArgumentException("Both startDate and endDate must be provided");
    }
    
    Calendar startCalendar = Calendar.getInstance();
    startCalendar.setTime(startDate);
    
    int startDateDayOfMonth = startCalendar.get(Calendar.DAY_OF_MONTH);
    int startDateTotalMonths = 12 * startCalendar.get(Calendar.YEAR) 
      + startCalendar.get(Calendar.MONTH);
        
    Calendar endCalendar = Calendar.getInstance();
    endCalendar.setTime(endDate);
    
    int endDateDayOfMonth = endCalendar.get(Calendar.DAY_OF_MONTH);
    int endDateTotalMonths = 12 * endCalendar.get(Calendar.YEAR) 
      + endCalendar.get(Calendar.MONTH);
    return (startDateDayOfMonth > endDateDayOfMonth) 
      ? (endDateTotalMonths - startDateTotalMonths) - 1 
      : (endDateTotalMonths - startDateTotalMonths);
}

Here, we add a condition to check if the start date day value is greater than the end date day value. Then, we adjust the month interval based on the condition.

Here’s a unit test for the method:

@Test
void whenCalculatingMonthsBetweenUsingLegacyDateApiDayValueConsidered_thenReturnMonthsDifference() throws ParseException {
    MonthInterval monthDifference = new MonthInterval();
    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
    
    Date startDate = sdf.parse("2016-05-31");
    Date endDate = sdf.parse("2016-11-28");
    int monthsBetween = monthDifference.monthsBetweenWithDayValue(startDate, endDate);
    
    assertEquals(5, monthsBetween);
}

Since the end date day value is less than the start date day value, the final month doesn’t count as a full month.

4. Using the Date Time API

Since Java 8, the Date Time API provides options to get the month interval between two dates. We can use the Period class and ChronoUnit enum to compute the month interval between two dates.

4.1. The Period Class

The Period class provides a static method named between() to calculate month intervals between two dates. It accepts two arguments representing the start date and the end date. The day value of the date impacts the month interval when using the Period class:

@Test
void whenCalculatingMonthsBetweenUsingPeriodClass_thenReturnMonthsDifference() {
    Period diff = Period.between(LocalDate.parse("2023-05-25"), LocalDate.parse("2023-11-23"));
    assertEquals(5, diff.getMonths());
}

Considering the day’s value in the dates, between() returns a difference of five months. The day value of the end date is less than the day value of the start date. Hence, the final month doesn’t count as a full month.

However, if we intend to ignore the day value of the dates, we can set the LocalDate objects to the first day of the month:

@Test
void whenCalculatingMonthsBetweenUsingPeriodClassAndAdjsutingDatesToFirstDayOfTheMonth_thenReturnMonthsDifference() {
    Period diff = Period.between(LocalDate.parse("2023-05-25")
      .withDayOfMonth(1), LocalDate.parse("2023-11-23")
      .withDayOfMonth(1));
    assertEquals(6, diff.getMonths());
}

Here, we invoke the withDayOfMonth() method on the LocalDate object to set the dates to the beginning of the month.

4.2. The ChronoUnit Enum

The ChronoUnit enum provides a constant named MONTHS and a static method named between() to compute the month interval between two dates.

Similar to the Period class, the ChronoUnit enum also considers the day value in the two dates:

@Test
void whenCalculatingMonthsBetweenUsingChronoUnitEnum_thenReturnMonthsDifference() {
    long monthsBetween = ChronoUnit.MONTHS.between(
      LocalDate.parse("2023-05-25"), 
      LocalDate.parse("2023-11-23")
    );
    assertEquals(5, monthsBetween);
}

Also, if we intend to ignore the day value, we can set the date objects to the first day of the month using the withDayOfMonth() method:

@Test
void whenCalculatingMonthsBetweenUsingChronoUnitEnumdSetTimeToFirstDayOfMonth_thenReturnMonthsDifference() {
    long monthsBetween = ChronoUnit.MONTHS.between(LocalDate.parse("2023-05-25")
      .withDayOfMonth(1), LocalDate.parse("2023-11-23")
      .withDayOfMonth(1));
    assertEquals(6, monthsBetween);
}

Finally, we can also use YearMonth.from() method with ChronoUnit enum to ignore the day value:

@Test
void whenCalculatingMonthsBetweenUsingChronoUnitAndYearMonth_thenReturnMonthsDifference() {
    long diff = ChronoUnit.MONTHS.between(
      YearMonth.from(LocalDate.parse("2023-05-25")), 
      LocalDate.parse("2023-11-23")
    );
    assertEquals(6, diff);
}

In the code above, we use the YearMonth.from() method to consider only the year and month values while computing the month between the two dates.

5. Using the Joda Time Library

The Joda-Time library provides the Months.monthsBetween() method to find month intervals between two dates. To use the Joda-Time library, let’s add its dependency to the pom.xml:

<dependency>
    <groupId>joda-time</groupId>
    <artifactId>joda-time</artifactId>
    <version>2.12.5</version>
</dependency>

Just like the Date Time API, it considers the day value of the dates objects by default:

@Test
void whenCalculatingMonthsBetweenUsingJodaTime_thenReturnMonthsDifference() {
    DateTime firstDate = new DateTime(2023, 5, 25, 0, 0);
    DateTime secondDate = new DateTime(2023, 11, 23, 0, 0);
    int monthsBetween = Months.monthsBetween(firstDate, secondDate).getMonths();
    assertEquals(5, monthsBetween);
}

In the code above, we create two date objects with a set date. Next, we pass the date objects to the monthsBetween() method and invoke getMonths() on it.

Finally, we assert that the return month interval is equal to the expected value.

If we intend not to consider the day value, we can invoke the withDayOfMonth() method on the DateTime objects and set the day to the first day of the month:

@Test
void whenCalculatingMonthsBetweenUsingJodaTimeSetTimeToFirstDayOfMonth_thenReturnMonthsDifference() {
    DateTime firstDate = new DateTime(2023, 5, 25, 0, 0).withDayOfMonth(1);
    DateTime secondDate = new DateTime(2023, 11, 23, 0, 0).withDayOfMonth(1);
        
    int monthsBetween = Months.monthsBetween(firstDate, secondDate).getMonths();
    assertEquals(6, monthsBetween);
}

Here, we set the day of the two date objects to the first day of the month to avoid the day value impacting the expected result.

6. Conclusion

In this article, we learned ways of calculating the month interval between two date objects using the standard library and a third-party library. Also, we saw how to calculate month intervals between dates with and without considering the day value of the dates.

The choice to include or ignore the day value depends on our goal and specific requirements. Also, the Date Time API provides different options, and it’s recommended because it’s easier to use and doesn’t require external dependency.

As usual, the complete source code for the examples is available over on GitHub.

       

Convert Byte Array to JSON and Vice Versa in Java

$
0
0

1. Introduction

The manipulation and convеrsion of data between different formats are common tasks in most programming languages. One such scenario involves converting data between a byte array and JSON format.

In this tutorial, we’ll explore how to convert a byte array to JSON and vicе vеrsa in Java.

2. Problem Statement

Wе aim to convеrt a JSON string into a bytе array, whеrе еach еlеmеnt of thе array rеprеsеnts thе ASCII valuе of thе corrеsponding charactеr in thе string. Convеrsеly, wе also sееk to convеrt a bytе array of ASCII valuеs back into thе original JSON string.

Suppose we have the following byte array:

byte[] byteArray = {
    34, 123, 92, 34, 110, 97, 109, 101, 92, 34, 58, 92, 34, 65, 108,
    105, 99, 101, 92, 34, 44, 92, 34, 97, 103, 101, 92, 34, 58, 50, 53, 44, 92,
    34, 105, 115, 83, 116, 117, 100, 101, 110, 116, 92, 34, 58, 116, 114, 117,
    101, 44, 92, 34, 104, 111, 98, 98, 105, 101, 115, 92, 34, 58, 91, 92, 34,
    114, 101, 97, 100, 105, 110, 103, 92, 34, 44, 92, 34, 112, 97, 105, 110,
    116, 105, 110, 103, 92, 34, 93, 44, 92, 34, 97, 100, 100, 114, 101, 115,
    115, 92, 34, 58, 123, 92, 34, 99, 105, 116, 121, 92, 34, 58, 92, 34, 83,
    109, 97, 108, 108, 118, 105, 108, 108, 101, 92, 34, 44, 92, 34, 122, 105,
    112, 99, 111, 100, 101, 92, 34, 58, 92, 34, 49, 50, 51, 52, 53, 92, 34, 125, 125, 34
};

This byte array corresponds to the following JSON string:

String jsonString = "{\"name\":\"Alice\",\"age\":25,\"isStudent\":true,\"hobbies\":
[\"reading\",\"painting\"],\"address\":{\"city\":\"Smallville\",\"zipcode\":\"12345\"}}";

This JSON string is visually represented as:

{
  "name": "Alice",
  "age": 25,
  "isStudent": true,
  "hobbies": ["reading", "painting"],
  "address": {
    "city": "Smallville",
    "zipcode": "12345"
  }
}

Next, we’ll explore several approaches to achieve the conversion between byte array and JSON string.

3. Converting Byte Array to JSON

Convеrting a bytе array to JSON is a crucial opеration whеn dеaling with data intеrchangе in Java. Furthermore, there are multiple approaches to achiеvе this convеrsion.

3.1. Using Standard Libaraies

We can utilize the Jackson as a standard library. Hеrе’s a simplе еxamplе of using Jackson to convеrt a bytе array to JSON:

@Test
void givenByteArray_whenConvertingToJsonUsingJackson_thenJsonString() throws Exception {
    ObjectMapper objectMapper = new ObjectMapper();
    String actualJsonString = objectMapper.readValue(byteArray, String.class);
    assertEquals(jsonString, actualJsonString);
}

In this еxamplе, we create an ObjеctMappеr object. Then we use thе rеadValuе() mеthod to convеrt thе bytеArray into a JSON string. Afterward, we compare the actualJsonString to an еxpеctеd jsonString using thе assеrtEquals mеthod to еnsurе that thе convеrsion is pеrformеd corrеctly.

3.2. Using External Libaraies

Bеsidеs thе standard librariеs, thеrе arе еxtеrnal librariеs that offеr additional fеaturеs and customization options. One such library is Gson.

Hеrе’s an еxamplе using Gson to achiеvе thе convеrsion:

@Test
void givenByteArray_whenConvertingToJsonUsingGson_thenJsonString() {
    Gson gson = new Gson();
    String jsonStringFromByteArray = new String(byteArray, StandardCharsets.UTF_8);
    String actualJsonString = gson.fromJson(jsonStringFromByteArray, String.class);
    assertEquals(jsonString, actualJsonString);
}

Here, we create a Gson object. Then, we convert thе bytеArray to a string using thе String constructor with еncoding spеcifiеd as UTF-8. Moreover, we utilize the fromJson() method to convеrt thе bytеArray into a JSON string.

4. Converting JSON to Byte Array

Like convеrting a bytе array to JSON, convеrting JSON data back to a bytе array is еssеntial for handling various data scеnarios.

4.1. Using Standard Libaraies

Java’s standard librariеs, such as Jackson, also support convеrting JSON strings to bytе arrays. Hеrе’s an еxamplе:

@Test
void givenJsonString_whenConvertingToByteArrayUsingJackson_thenByteArray() throws JsonProcessingException {
    ObjectMapper objectMapper = new ObjectMapper();
    byte[] actualByteArray = objectMapper.writeValueAsBytes(jsonString);
    assertEquals(Arrays.toString(byteArray), Arrays.toString(actualByteArray));
}

In this example, we utilize the writeValueAsBytes() method to convert the defined jsonString at the class level to a bytе array.

4.2. Using External Libaraies

Extеrnal librariеs, such as Gson, can also bе еmployеd for convеrting JSON to bytе arrays. Hеrе’s an еxamplе:

@Test
void givenJsonString_whenConvertingToByteArrayUsingGson_thenByteArray() {
    Gson gson = new Gson();
    byte[] actualByteArray = gson.toJson(jsonString).getBytes();
    assertEquals(Arrays.toString(byteArray), Arrays.toString(actualByteArray));
}

Here, we employ the toJson() method to accomplish the conversion.

5. Conclusion

In conclusion, understanding bytе array to JSON convеrsion and vise verse is crucial in Java for vеrsatilе data handling. Whеthеr with standard librariеs likе Jackson or еxtеrnal onеs likе Gson, we can еfficiеntly managе data intеrchangе in thеir applications.

As usual, the accompanying source code can be found over on GitHub.

       

Comparison of for Loops and Iterators

$
0
0

1. Introduction

for loops and Iterators, both provide mechanisms to traverse through collections of elements. While both serve the purpose of iterating over collections, they differ in their syntax, functionality, and applicability.

In this tutorial, we’ll explore a detailed comparison between for loops and iterators, highlighting their key distinctions in several aspects.

We’ll use the following list of strings to demonstrate:

List<String> names = Arrays.asList("Alice", "Bob", "Charlie");

2. Forward Traversal

In this section, we’ll explore the forward traversal methods for both for loops and iterators.

2.1. With for Loops

Traditional for loops in Java are designed for forward iteration. They start from an initial index and move toward the end of the collection, processing each element in sequential order.

Let’s iterate forward using a for loop:

StringBuilder stringBuilder = new StringBuilder(); 
for (int i = 0; i < names.size(); i++) { 
    stringBuilder.append(names.get(i)); 
} 
assertEquals("AliceBobCharlie", stringBuilder.toString());

2.2. With Iterators

Iterators, by default, offer forward-only traversal. The hasNext() method checks for the existence of the next element, and the next() method moves the iterator to the next position in the collection:

StringBuilder stringBuilder = new StringBuilder();
Iterator<String> iterator = names.iterator();
while (iterator.hasNext()) {
    stringBuilder.append(iterator.next());
}
assertEquals("AliceBobCharlie", stringBuilder.toString());

3. Backward Traversal

In this section, we’ll explore the backward traversal methods for both for loops and iterators.

3.1. With for Loops

While it’s possible to simulate backward traversal by manipulating the for loop variable, it isn’t as straightforward as forward iteration. Let’s iterate backward using a for loop:

StringBuilder stringBuilder = new StringBuilder();
for (int i = names.size() - 1; i >= 0; i--) {
    stringBuilder.append(names.get(i));
}
assertEquals("CharlieBobAlice", stringBuilder.toString());

3.2. With Iterators

However,  if a collection implements the List interface and provides a ListIterator, we can achieve backward iteration using the hasPrevious() and previous() methods:

StringBuilder stringBuilder = new StringBuilder();
ListIterator<String> listIterator = names.listIterator(names.size());
while (listIterator.hasPrevious()) {
    stringBuilder.append(listIterator.previous());
}
assertEquals("CharlieBobAlice", stringBuilder.toString());

4. Removal of Elements

In this section, we’ll explore the remove methods in both for loops and iterators.

4.1. With for Loops

for loops aren’t directly compatible with removing elements from the collection being traversed. Modifying the collection during a for loop iteration can lead to unpredictable behavior as the size of the collection is modified. This often results in ConcurrentModificationException or incorrect indices.

Let’s test out the remove() method during looping:

assertThrows(ConcurrentModificationException.class, () -> {
    for (String name : names) {
        names.remove("Bob");
    }
});

4.2. With Iterators

Iterators, on the other hand, provide a safe and reliable way to remove elements during iteration using the remove() method. Iterator internally maintains a cursor or a position within the collection. When we call remove(), it knows exactly which element to remove based on its internal state. This prevents concurrent modification issues and ensures the integrity of the iteration process.

Let’s test out the remove() method with Iterator:

Iterator<String> iterator = names.iterator();
while (iterator.hasNext()) {
    String name = iterator.next();
    if (name.equals("Bob")) {
        iterator.remove();
    }
}
List<String> expected = Arrays.asList("Alice", "Charlie");
assertIterableEquals(expected, names);

5. Flexibility

In this section, we’ll explore the flexibility to alter elements during iteration in both for loops and iterators.

5.1. With for Loops

for loops provide direct access to the elements of a collection based on their indices. This offers flexibility in terms of modification and access, as we have explicit control over the index and can easily perform insertions and modification operations:

for (int i = 0; i < names.size(); i++) {
    names.set(i, names.get(i).toLowerCase());
}
List<String> expected = Arrays.asList("alice","bob", "charlie");
assertIterableEquals(expected, names);

5.2. With Iterators

Iterators, while excellent for traversal and removal, don’t provide direct access to index-based operations. The Iterator interface focuses on forward-only traversal and removal, limiting the ability to directly insert or modify elements. If we need to add or modify elements using Iterator, we may want to consider ListIterator.

6. Error-proneness

for loops are more prone to errors due to their reliance on index-based access. Incorrect index values or modifications to the collection during iteration can lead to various exceptions and unexpected behavior. For example, for loop can lead to IndexOutOfBoundException if the index value is outside the bounds of the collection. This can happen if the index variable isn’t properly initialized or if the collection size is modified during iteration.

On the other hand, Iterator enforces hasNext() checks before accessing elements, preventing null pointer exceptions. This ensures that the Iterator points to a valid element before attempting to access it.

7. Code Readability

for loops are generally considered more readable and concise for simple iterations over collections due to their straightforward syntax. The loop structure clearly conveys the iteration logic, with the index variable explicitly indicating the current position in the collection. This makes it easy to understand the code and follow the flow of the iteration.

While Iterator offers benefits for complex scenarios, it can introduce some readability challenges for simple iterations. Iterators require method calls like hasNext() or next() to iterate through the collection. These method calls can introduce additional complexity and make the iteration logic less clear compared to the concise syntax of a for loop.

8. Choosing Between Iterators and for Loops

In summary, for loops are suitable for simple iteration, especially when direct access to indices is beneficial.

Iterators, on the other hand, are powerful when dealing with safe removal, forward-only traversal, and when working with various collection types.

The following table shows the main differences between the for loop and Iterator:

Feature for Loop Iterator
Traversal Direction Forward and backward using indexing Forward (default), bidirectional with ListIterator
Element Removal Not directly compatible, can lead to errors Safe and reliable using remove() method
Flexibility – Insert, Access, Modify Direct index-based access Limited to forward-only traversal and removal; ListIterator for modification while iterating
Error-proneness More prone to errors due to index-based access and potential modifications Enforce hasNext() checks, reducing null pointer exceptions

9. Conclusion

In this article, we discussed the difference between for loops and an Iterators.

for loops provides a straightforward approach for simple forward traversal, while Iterators are powerful when dealing with safe removal and forward-only traversal.

As always, the source code for the examples is available over on GitHub.

       

How to Write Strings to OutputStream in Java

$
0
0

1. Overview

We often work with OutputStream when we need to transfer data to external destinations such as files and networks. Data can either be in binary or string format. We use OutputStream, which is a byte stream, to deal with binary data, and Writer, which is a character stream, to deal with string data.

However, there are situations where we have to write strings into an OutputStream due to the limitations imposed by the selected API. In some instances, the API may only provide OutputStream instead of Writer. In this tutorial, we’ll explore different methods for writing strings into OutputStream in such scenarios.

2. Byte Conversion

The most intuitive approach involves converting the string into bytes and then writing the converted bytes into the OutputStream:

String str = "Hello";
byte[] bytes = str.getBytes(StandardCharsets.UTF_8);
outputStream.write(bytes);

This approach is straightforward but has a major drawback. We need to explicitly convert the string into bytes beforehand and specify the character encoding each time when calling getBytes(). This makes our code cumbersome.

3. OutputStreamWriter

A better approach is to utilize OutputStreamWriter to wrap our OutputStream. OutputStreamWriter serves as a wrapper that transforms a character stream into a byte stream. Strings written to it are encoded into bytes using the chosen character encoding.

We write the string via the write() method without specifying the character encoding. Each invocation of write() on OutputStreamWriter implicitly converts the string into encoded bytes, providing a more streamlined process:

try (OutputStreamWriter writer = new OutputStreamWriter(outputStream, StandardCharsets.UTF_8)) {
    writer.write("Hello");
}

If the wrapped OutputStream is not yet buffered, we advise using the BufferedWriter to wrap around the OutputStreamWriter to make the writing process more efficient:

BufferedWriter bufferedWriter = new BufferedWriter(writer);

This reduces IO operations by first writing data to a buffer and then flushing the buffered data to the stream in a larger chunk.

4. PrintStream

Another option is to utilize PrintStream, which provides functionality similar to OutputStreamWriter. Similar to OutputStreamWriter, we can specify the encoding when we instantiate PrintStream:

try (PrintStream printStream = new PrintStream(outputStream, true, StandardCharsets.UTF_8)) {
    printStream.print("Hello");
}

If we don’t define the character encoding explicitly in the constructor, the default encoding will be UTF-8:

PrintStream printStream = new PrintStream(outputStream);

The difference between PrintStream and OutputStreamWriter is that PrintStream provides additional print() methods for writing different data types to the OutputStream. Also, PrintWriter never throws an IOException for its print() and write() methods:

printStream.print(100); // integer
printStream.print(true); // boolean

5. PrintWriter

PrintWriter serves a similar purpose to PrintStream, offering functionality for writing formatted representations of data to an OutputStream:

try (PrintWriter writer = new PrintWriter(outputStream)) {
    writer.print("Hello");
}

Besides wrapping OutputStream, PrintWriter provides additional constructors to wrap Writer as well. Another difference between them is that PrintWriter provides write() methods for writing character arrays, whereas PrintStream provides write() methods for writing byte arrays:

char[] c = new char[] {'H', 'e', 'l' ,'l', 'o'};
try (PrintWriter writer = new PrintWriter(new StringWriter())) {
    writer.write(c);
}

6. Conclusion

In this article, we’ve explored several approaches for writing strings to an OutputStream in Java.

We began with the straightforward conversion of strings into bytes, which requires explicit encoding for each write operation and impacts the code’s maintainability. Subsequently, we explored three different Java classes that wrap around the OutputStream to encode strings into bytes seamlessly.

As always, the sample code is available over on GitHub.

       

Parse Java Source Code and Extract Methods

$
0
0

1. Introduction

In this article, we’re going to investigate the JavaCompiler API. We’ll see what this API is, what we can do with it, and how to use it to extract the details of methods defined in our source files.

2. The JavaCompiler API

Java 6 introduced the ToolProvider mechanism, which gives us access to various built-in JVM tools. Amongst other things, this includes the JavaCompiler. This is the same functionality as in the javac application, which is only programmatically available.

Using this, we can compile Java source code. However, we can also extract information from the code as part of the compilation process.

To get access to the JavaCompiler, we need to use the ToolProvider, which will give us an instance if available:

JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();

Note that there’s no guarantee that the JavaCompiler will be available. It depends on the JVM being used and what tooling it makes available.

However, interrogating the Java code instead of simply compiling it is implementation-dependent. In this article, we’re assuming the use of the Oracle compiler and that the tools.jar file is available on the classpath. Note that since Java 9, this file is no longer available by default, so we need to make sure an appropriate version is available for use.

3. Processing Java Code

Once a JavaCompiler instance is available, we can process some Java code. We need an appropriate JavaFileManager instance and an appropriate collection of JavaFileObject instances to do this. Exactly how we do both of these things depends on the source of the code that we wish to process.

If we want to process code that exists as files on disk, we can rely on the JVM tooling. In particular, the StandardJavaFileManager that the JavaCompiler instance provides access to is intended precisely for this purpose:

StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, StandardCharsets.UTF_8);

Once we’ve got this, we can then use it to access the files that we want to process:

Iterable<? extends JavaFileObject> compilationUnits = fileManager.getJavaFileObjectsFromFiles(Arrays.asList(new File(filename)));

We can use other instances of these if we need to. For example, if we want to process code held in local variables,

Once we have these, we can then process our files:

JavacTask javacTask = 
  (JavacTask) compiler.getTask(null, fileManager, null, null, null, compilationUnits);
Iterable<? extends CompilationUnitTree> compilationUnitTrees = javacTask.parse();

Note that we’re casting the compiler’s result.getTask() into a JavacTask instance. This class exists in the tools.jar file and is the entry point to interrogating the processed Java source. We then use this to parse our input files into a collection of CompilationUnitTree types. Each of these represents on file that we provided to the compiler.

4. Compilation Unit Details

Once we’ve got this far, we have the parsed details of the compilation unit – that is, the source files that we’ve processed – available to us.

The first thing that we can do is to interrogate the top-level details. For example, we can see what package it represents using getPackageName() and get the list of imports using getImports(). We can also use getTypeDecls() to get the list of all top-level declarations – which typically means the class definitions but could be anything the Java language supports.

We’ll notice here that everything returned is an implementation of the Tree interface. The entire compilation unit is represented as a tree structure, allowing things to be appropriately nested. For example, it’s legal to have class definitions nested inside methods where the method is already nested inside another class.

One advantage this gives us is that the Tree structure implements the visitor pattern. This allows us to have code that can interrogate any instance of the structure without knowing ahead of time what it is.

This is very useful since getTypeDecls() returns a collection of arbitrary Tree types, so we don’t know at this point what we’re dealing with:

for (Tree tree : compilationUnitTree.getTypeDecls()) {
    tree.accept(new SimpleTreeVisitor() {
        @Override
        public Object visitClass(ClassTree classTree, Object o) {
            System.out.println("Found class: " + classTree.getSimpleName());
            return null;
        }
    }, null);
}

We can also determine the type of our Tree instances by querying it directly. All of our Tree instances have a getKind() method that returns an appropriate value from the Kind enumeration. For example, class definitions will return Kind.CLASS to indicate that that’s what they are.

We can then use this and cast the value ourselves if we don’t want to use the visitor pattern:

for (Tree tree : compilationUnitTree.getTypeDecls()) {
    if (tree.getKind() == Tree.Kind.CLASS) {
        ClassTree classTree = (ClassTree) tree;
        System.out.println("Found class: " + classTree.getSimpleName());
    }
}

5. Class Details

Once we’ve got access to a ClassTree instance – however we manage that – we can start to interrogate this for details about the class definition. This includes class-level details such as the class name, the superclass, the list of interfaces, and so on.

We can also get the class members’ details – using getMembers(). This includes anything that can be a class member, such as methods, fields, nested classes, etc. Anything that you’re allowed to write directly into the body of a class will be returned by this.

This is the same as we saw with CompilationUnitTree.getTypeDecls(), where we can get a mixture of different types. As such, we need to treat it similarly, using the visitor pattern or the getKind() method.

For example, we can extract all of the methods out of a class:

for (Tree member : classTree.getMembers()) {
    member.accept(new SimpleTreeVisitor(){
        @Override
        public Object visitMethod(MethodTree methodTree, Object o) {
            System.out.println("Found method: " + methodTree.getName());
            return null;
        }
    }, null);
}

6. Method Details

If we wish, we can interrogate the MethodTree instance to get more information about the method itself. As we’d expect, we can get all the details about the method signature. This includes the method name, parameters, return type, and throws clause, but also details like generic type parameters, modifiers, and even – in the case of methods present in annotation classes – the default value.

As always, everything we’re given here is a Tree or some subclass. For example, the method parameters are always VariableTree instances because that’s the only legal thing in that position. We can then treat these as any other part of the source file.

For example, we can print out some of the details of a method:

System.out.println("Found method: " + classTree.getSimpleName() + "." + methodTree.getName());
System.out.println("Return value: " + methodTree.getReturnType());
System.out.println("Parameters: " + methodTree.getParameters());

Which will produce output such as:

Found method: ExtractJavaLiveTest.visitClassMethods
Return value: void
Parameters: ClassTree classTree

7. Method Body

We can go even further than this, though. The MethodTree instance gives us access to the parsed body of the method as a collection of statements.

This, more than anywhere else in the API, is where the fact that everything is a Tree really benefits us. In Java, there are a variety of statements that have special details about them, which can even include some statements containing other statements.

For example, the following Java code is a single statement:

for (Tree statement : methodTree.getBody().getStatements()) {
    System.out.println("Found statement: " + statement);
}

This statement is an “Enhanced for loop” and consists of:

  • A variable declaration – Tree statement
  • An expression – methodTree.getBody().getStatements()
  • A nested statement – The block containing System.out.println(“Found statement: ” + statement);

Our JavaCompiler represents this as an EnhancedForLoopTree instance, which gives us access to these different details. Every different type of statement that can be used in Java is represented by a subclass of StatementTree, allowing us to get the pertinent details back out again.

8. Future Proofing

Java pays a lot of attention to backward compatibility. However, forward compatibility is less well managed. This means it’s possible to have Java code that uses syntax our program doesn’t expect. For example, Java 5 introduced the enhanced for loop. We’d be surprised to see one of these if we were expecting code older than that.

However, all this means is that we must be prepared for Tree instances that we might not expect. Depending on exactly what we’re doing, this might be a serious concern, or it might not even be an issue. In general, though, we should be prepared to fail if we’re trying to parse Java code from a version newer than we’re expecting.

9. Conclusion

We’ve seen how to use the JavaCompiler API to parse some Java source code and get information from it. In particular, we’ve seen how to get from the source file to the individual statements that make up method bodies.

You can do much more with this API, so why not try some of it out yourself?

As always, all of the code from this article can be found over on GitHub.

       

UTF-8 Validation in Java

$
0
0

1. Overview

In data transmission, we often need to handle byte data. If the data is an encoded string instead of a binary, we often encode it in Unicode. Unicode Transformation Format-8 (UTF-8) is a variable-length encoding that can encode all possible Unicode characters.

In this tutorial, we’ll explore the conversion between UTF-8 encoded bytes and string. After that, we’ll dive into the crucial aspects of conducting UTF-8 validation on byte data in Java.

2. UTF-8 Conversion

Before we jump into the validation sections, let’s review how to convert a string into a UTF-8 encoded byte array and vice versa.

We can simply call the getBytes() method with the target encoding of a string to convert a string into a byte array:

String UTF8_STRING = "Hello 你好";
byte[] UTF8_BYTES = UTF8_STRING.getBytes(StandardCharsets.UTF_8);

For the reverse, the String class provides a constructor to create a String instance by a byte array and its source encoding:

String decodedStr = new String(array, StandardCharsets.UTF_8);

The constructor we used doesn’t have much control over the decoding process. Whenever the byte array contains unmappable character sequences, it replaces those characters with the default replacement character �:

@Test
void whenDecodeInvalidBytes_thenReturnReplacementChars() {
    byte[] invalidUtf8Bytes = {(byte) 0xF0, (byte) 0xC1, (byte) 0x8C, (byte) 0xBC, (byte) 0xD1};
    String decodedStr = invalidUtf8Bytes.getBytes(StandardCharsets.UTF_8);
    assertEquals("�����", decodedStr);
}

Therefore, we cannot use this method to validate whether a byte array is encoded in UTF-8.

3. Byte Array Validation

Java provides a simple way to validate whether a byte array is UTF-8 encoded using CharsetDecoder:

CharsetDecoder charsetDecoder = StandardCharsets.UTF_8.newDecoder();
CharBuffer decodedCharBuffer = charsetDecoder.decode(java.nio.ByteBuffer.wrap(UTF8_BYTES));

If the decoding process succeeds, we consider those bytes as valid UTF-8. Otherwise, the decode() method throws MalformedInputException:

@Test
void whenDecodeInvalidUTF8Bytes_thenThrowsMalformedInputException() {
    CharsetDecoder charsetDecoder = StandardCharsets.UTF_8.newDecoder();
    assertThrows(MalformedInputException.class,() -> {
        charsetDecoder.decode(java.nio.ByteBuffer.wrap(INVALID_UTF8_BYTES));
    });
}

4. Byte Stream Validation

When our source data is a byte stream rather than a byte array, we can read the InputStream and put its content into a byte array. Subsequently, we can apply the encoding validation on the byte array.

However, our preference is to directly validate the InputStream. This avoids creating an extra byte array and reduces the memory footprint in our application. It’s particularly important when we process a large stream.

In this section, we’ll define the following constant as our source UTF-8 encoded InputStream:

InputStream UTF8_INPUTSTREAM = new ByteArrayInputStream(UTF8_BYTES);

4.1. Validation Using Apache Tika

Apache Tika is an open-source content analysis library that provides a set of classes for detecting and extracting text content from different file formats.

We need to include the following Apache Tika core and standard parser dependencies in pom.xml:

<dependency>
    <groupId>org.apache.tika</groupId>
    <artifactId>tika-core</artifactId>
    <version>2.9.1</version>
</dependency>
<dependency>
    <groupId>org.apache.tika</groupId>
    <artifactId>tika-parsers-standard-package</artifactId>
    <version>2.9.1</version>
</dependency>

When we conduct a UTF-8 validation in Apache Tika, we instantiate a UniversalEncodingDetector and use it to detect the encoding of the InputStream. The detector returns the encoding as a Charset instance. We simply verify whether the Charset instance is a UTF-8 one:

@Test
void whenDetectEncoding_thenReturnsUtf8() {
    EncodingDetector encodingDetector = new UniversalEncodingDetector();
    Charset detectedCharset = encodingDetector.detect(UTF8_INPUTSTREAM, new Metadata());
    assertEquals(StandardCharsets.UTF_8, detectedCharset);
}

It’s worth noting that when we detect a stream that contains only the first 128 characters in the ASCII code, the detect() method returns ISO-8859-1 instead of UTF-8.

ISO-8859-1 is a single-byte encoding to represent ASCII characters, which are the same as the first 128 Unicode characters. Due to this characteristic, we still consider the data to be UTF-8 encoded if the method returns ISO-8859-1.

4.2. Validation Using ICU4J

ICU4J stands for International Components for Unicode for Java and is a Java library published by IBM. It provides Unicode and globalization support for software applications. We need the following ICU4J dependency in our pom.xml:

<dependency>
    <groupId>com.ibm.icu</groupId>
    <artifactId>icu4j</artifactId>
    <version>74.1</version>
</dependency>

In ICU4J, we create a CharsetDetector instance to detect the charset of the InputStream. Similar to the validation using Apache Tika, we verify whether the charset is UTF-8 or not:

@Test
void whenDetectEncoding_thenReturnsUtf8() {
    CharsetDetector detector = new CharsetDetector();
    detector.setText(UTF8_INPUTSTREAM);
    CharsetMatch charsetMatch = detector.detect();
    assertEquals(StandardCharsets.UTF_8.name(), charsetMatch.getName());
}

ICU4J exhibits the same behavior when it detects the encoding of the stream where the detection returns ISO-8859-1 when the data contains only the first 128 ASCII characters.

5. Conclusion

In this article, we’ve explored UTF-8 encoded bytes and string conversion and different types of UTF-8 validation based on byte and stream. This journey equips us with practical code to foster a deeper understanding of UTF-8 in Java applications.

As always, the sample code is available over on GitHub.

       

Difference Between execute() and submit() in Executor Service

$
0
0

 1. Overview

Multithreading and parallel processing are crucial concepts in modern application development. In Java, the Executor framework provides a way to manage and control the execution of concurrent tasks efficiently. The ExecutorService interface is at the core of this framework, and it provides two commonly used methods for submitting tasks to be executed: submit() and execute().

In this article, we’ll explore the key differences between these two methods. We’ll look at both submit() and execute() using a simple example where we want to simulate a task of calculating the sum of numbers in an array using a thread pool.

2. Usage of ExecutorService.submit( )

Let’s start with the submit() method first which is widely used in the ExecutorService interface. It allows us to submit tasks for execution and returns a Future object that represents the result of the computation.

This Future then allows us to obtain the result of the computation, handle exceptions that occur during task execution, and monitor the status of the task. We can call get() in the Future to retrieve the result or exceptions.

Let’s start by initializing the ExecutorService:

ExecutorService executorService = Executors.newFixedThreadPool(2);

Here, we’re initializing the ExecutorService with a fixed thread pool of size 2. This creates a thread pool that utilizes a fixed number of threads operating off a shared unbounded queue. In our case, at any point, at most two threads will be active processing tasks. If more tasks are sent while all existing tasks are being processed, they will be held in the queue until a processing thread becomes free.

Next, let’s create a task using Callable:

Callable<Integer> task = () -> {
    int[] numbers = {1, 2, 3, 4, 5};
    int sum = 0;
    for (int num : numbers) {
        sum += num;
    }
    return sum;
};

Importantly, here, the Callable object represents the task that returns a result and may throw an exception. In this case, it represents a task that another thread can execute and return a result or may throw exceptions. This Callable calculates the sum of integers in an array and returns the result.

Now that we’ve defined the task as Callable, let’s submit this task to the ExecutorService:

Future<Integer> result = executorService.submit(task);

Simply put, the submit() method takes the Callable task and submits it for execution by the ExecutorServiceIt returns a Future<Integer> object that represents the future result of the computation. Overall, executorService.submit() is a way to asynchronously execute a Callable task using ExecutorService allowing for concurrent execution of tasks and obtaining their results via the returned Future

Finally, let’s check the result:

try {
    int sum = result.get();
    System.out.println("Sum calculated using submit:" + sum);
} catch (InterruptedException | ExecutionException e) {
    e.printStackTrace();
}

Here, get() retrieves the result of the task execution. In this case, it fetches the sum calculated by the task and prints it. However, it’s important to note that the get() method is a blocking call and waits if the result isn’t available yet, potentially causing the thread to pause until the result is ready. It can also throw exceptions like InterruptedException or ExecutionException if the computation encounters issues while running.

Finally, let’s shut down the ExecutorService:

executorService.shutdown();

This shuts down the ExecutorService after the task has completed execution and releases any resources used by the service.

3.  Usage of ExecutorService.execute( )

The execute() method is a simpler method, defined in the Executor interface which is a parent interface of ExecutorService. It’s used to submit tasks for execution but doesn’t return a Future. This means that we cannot obtain the result of the task or handle exceptions directly through the Future object.

It’s suitable for scenarios where we don’t need to wait for the result of the task and we don’t expect any exceptions. These tasks are executed for their side effect.

Like before, we’ll create an ExecutorService with a fixed thread pool of 2.  However, we’ll create a task as Runnable instead:

Runnable task = () -> {
    int[] numbers = {1, 2, 3, 4, 5};
    int sum = 0;
    for (int num : numbers) {
        sum += num;
    }
    System.out.println("Sum calculated using execute: " + sum);
};

Importantly, the task doesn’t return any result; it simply calculates the sum and prints it inside the task. We’ll  now submit the Runnable task to the ExecutorService:

executorService.execute(task);

This remains an asynchronous operation, indicating that one of the threads from the thread pool executes the task.

4. Conclusion

In this article, we looked at the salient features and usages of submit() and execute() from the ExecutorService interface. In summary, both of these methods offer a way to submit tasks for concurrent execution, but they differ in their handling of task results and exceptions.

The choice between submit() and execute() depends on specific requirements. If we need to obtain the result of a task or handle exceptions, we should use submit(). On the other hand, if we have a task that doesn’t return a result and we want to fire it and forget it, execute() is the right choice.

Moreover, if we’re working with a more complex use case and need the flexibility to manage tasks and retrieve results or exceptions, submit() is the better choice.

As always, the complete code for this article is available over on GitHub.

       

Print a Double Value Without Scientific Notation in Java

$
0
0

1. Introduction

In our daily tasks, we may often come across challenges to format double values. One such challenge could be printing a double value, avoiding scientific notation. Even though this method helps us express large and small values more compactly, there are situations where the default scientific notation might not be the most practical choice. In such cases, alternative approaches may need to be considered for a more suitable representation.

In this tutorial, we’ll explore various methods to print a double value without scientific notation in Java.

2. About Scientific Notation

Scientific notation consists of two components: a coefficient and an exponent. Typically, a decimal number between 1 and 10 is the coefficient, while the exponent indicates the power of 10 by which the system multiplies the coefficient.

In Java, scientific notation is often represented using the “e” notation, where “e” stands for exponent:

double largeNumber = 256450000d;
System.out.println("Large Number: " + largeNumber);
double smallNumber = 0.0000046d;
System.out.println("Small Number: " + smallNumber);

Running the code from above will output to the console two scientifically notated numbers:

Large Number: 2.5645E8
Small Number: 4.6E-6

The first number was represented as 2.5645 * 108, and the second one as 4.6 * 10-6 using the “e” notation. This compact writing can help us many times, but it can also mislead us. Due to this, in the following, we’ll see some methods to eliminate it.

3. Using the DecimalFormat Class

We can easily control how numeric values are shown using the DecimalFormat class. Also, it allows us to specify formatting patterns. We can define the desired decimal places and other formatting details to suit our application’s requirements:

DecimalFormat df = new DecimalFormat("#.###########");
double largeNumber = 256450000d;
System.out.println("Large Number: " + df.format(largeNumber));
double smallNumber = 0.0000046d;
System.out.println("Small Number: " + df.format(smallNumber));

We created a pattern that consists of a sequence of special characters, possibly combined with text. In our examples, we opted for the ‘#’ character, which displays a digit when one is provided and nothing otherwise. If we run the code from above, we will print the numbers on the console without scientific notation:

Large Number: 256450000
Small Number: 0.0000046

We can see that even if we put only one character for the integer part, it will always be retained, regardless of whether the pattern is smaller than the actual number.

4. Using printf() Method

The printf() method in the PrintStream class offers a dynamic and straightforward way to format output. It closely resembles the traditional C-style printf() function.

We’ll use the %f specifier to shape the presentation of floating-point numbers assertively:

double largeNumber = 256450000d;
System.out.printf("Large Number: %.7f", largeNumber);
System.out.println();
double smallNumber = 0.0000046d;
System.out.printf("Small Number: %.7f", smallNumber);

In this case, we’ll output:

Large Number: 256450000.0000000
Small Number: 0.0000046

If the number has no decimals, it will fill the decimal places with zeros. Also, if we don’t specify the number of decimals, it will print, by default, six decimal places.

If we print our small number without explicitly specifying the number of decimals:

System.out.printf("Small Number: %f", smallNumber);

Because the value has more decimals than specified in the format, the printf() method will round the value:

Small Number: 0.000005

5. Using BigDecimal

BigDecimal is ideal when dealing with financial calculations, scientific computations, or any application where maintaining precision is crucial. Unlike primitive data types or double values, which can lead to precision loss due to the inherent limitations of binary floating-point representation, BigDecimal allows us to specify the exact precision we need.

When working with BigDecimal, the concept of scientific notation is inherently different. The class itself allows us to work with arbitrary precision numbers without resorting to scientific notation, providing a straightforward solution to the challenges posed by default representations:

double largeNumber = 256450000d;
System.out.println("Large Number: " + BigDecimal.valueOf(largeNumber).toPlainString());
double smallNumber = 0.0000046d;
System.out.println("Small Number: " + BigDecimal.valueOf(smallNumber).toPlainString());

In this example, we employ the BigDecimal.valueOf() method to convert a double value with scientific notation into a BigDecimal. Subsequently, the toPlainString() function converts our complex object into a printable string, providing a precise and easily interpretable representation.

Besides eliminating scientific notation, BigDecimal provides features such as customizable rounding modes, which can be crucial in financial applications. The ability to precisely control the number of decimal places and handle extremely large or small numbers makes BigDecimal a go-to choice for developers prioritizing accuracy and precision in their Java applications.

6. Using String.format()

The String.format() method shapes a String using a format string and arguments.

Both String.format() and printf() methods use the java.util.Formatter class and share the exact underlying mechanism for formatting, providing a convenient way to produce well-structured output. However, there are subtle differences in their application and usage.

double largeNumber = 256450000d;
String formattedLargeNumber = String.format("%.7f", largeNumber);
System.out.println("Large Number: " + formattedLargeNumber);
double smallNumber = 0.0000046d;
String formattedSmallNumber = String.format("%.7f", smallNumber);
System.out.println("Small Number: " + formattedSmallNumber);

As can be seen in the example above, the String.format() method returns a formatted string rather than directly printing it to the console. We can store the formatted string in a variable, therefore making it easier for us to handle it further or use it in different contexts. Both methods share the same format specifiers, so the syntax for formatting remains consistent between them.

7. Conclusion

In this article, we saw multiple avenues for avoiding scientific notation when printing double values. Even more, we’ve seen many ways to format numbers, small or large, using different methods. We also played with format specifiers to ensure our desired level of detail.

Ultimately, the choice between methods rests on personal preference and the specific needs of the task. As always, the complete source code for the examples is available over on GitHub.

       
Viewing all 4469 articles
Browse latest View live


<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>