Category: Programming Languages

Programming languages are the foundational building blocks that power the digital world around us. From the websites we browse to the apps we use daily, these languages serve as the medium through which software engineers and developers bring ideas to life.

Mastering the art of programming requires a deep understanding of various languages, each with its unique syntax, strengths, and applications. Whether you’re a seasoned coder or just starting your journey, exploring the diverse landscape of programming languages can open doors to a world of limitless possibilities.

In this category, we delve into the intricacies of popular and emerging programming languages, covering their key features, use cases, and the tools and frameworks that support them. Learn how languages like Python, Java, JavaScript, C++, Rust and many others are shaping the future of technology, and discover the best practices for leveraging their capabilities to create innovative digital solutions.

Dive into our curated content to gain insights into the latest trends, industry standards, and expert tips that will empower you to become a more proficient programmer, and unlock the full potential of the digital landscape.

  • Java programming language

    Java is a widely-used programming language that has gained immense popularity for its versatility, reliability, and portability. Developed in the mid-1990s by Sun Microsystems (now owned by Oracle Corporation), Java has become one of the most widely adopted programming languages worldwide.

    Platform independence

    One of Java’s standout features is its platform independence as it employs a unique approach where programs are compiled into bytecode. This can then be executed on any platform equipped with a Java Virtual Machine (JVM).

    This “write once, run anywhere” capability has been instrumental in making Java a favourite among developers across various operating systems and devices.

    Object-oriented programming paradigm

    Java places a strong emphasis on objects and classes for code organisation and structure. This approach in Java, enables the creation of modular and reusable code, facilitating easier maintenance and expansion of software projects.

    The language itself draws inspiration from C and C++. Earlier on Java’s life, programmers familiar with these languages would find Java’s syntax relatively straightforward. However, Java incorporates additional features that enhance security, such as built-in memory management and effective exception handling mechanisms.

    Built-in libraries

    Java boasts an extensive standard library that provides a wealth of pre-built classes and methods for common programming tasks. This inclusive library covers a wide range of functionalities, including input/output operations, networking, database connectivity, and graphical user interface (GUI) development. These resources allows developers to save time and effort while developing applications.

    Concurrency

    Another vital aspect of Java. It offers native support for multithreading, allowing for concurrent execution of multiple tasks. This feature is particularly advantageous for applications that require efficient handling of simultaneous operations, such as web servers or data processing systems.

    The Java Community

    Is vibrant and dynamic, featuring a vast ecosystem of frameworks, libraries, and resources. This thriving community encourages collaboration, knowledge-sharing, and continuous improvement. Developers can benefit from a plethora of online forums, extensive documentation, and open-source projects, facilitating learning and growth in the Java programming domain.

    Multiple domains

    Java can be used across multiple domains, including web development, mobile app development (especially with Android), enterprise software, scientific research, and game development. Its versatility and scalability make it suitable for projects of all sizes, ranging from small-scale applications to large enterprise systems.

    Summary

    Java is a versatile programming language known for its platform independence, object-oriented approach, more recently functional approach and comprehensive standard library.

    Its widespread adoption, robust community support, and cross-platform compatibility have made it a popular choice among developers worldwide.

    Whether one is a beginner or an experienced programmer, either learning it as first programming language or as a new language, Java offers a solid foundation for building a wide range of software applications.

  • What are Java EE / Jakarta Filters

    Both a Jakarta filter and a Java EE (Enterprise Edition) filter are components used in web applications to intercept and handle HTTP requests and responses.

    The development of Java EE technology has caused them to diverge.

    What is a Java EE Filter?

    Previously, Oracle Corporation created and supported the Java EE platform.

    The javax.servlet.Filter interface is used to implement Java EE filters.

    To pre-process and post-process web resources like servlets, JSPs (JavaServer Pages), or static files, available in Java EE web applications.

    Annotations and the deployment descriptor file (web.xml) can be used to specify Java EE filters.

    They give Java EE applications a method to implement common functionality across many web resources.

    What is a Jakarta Filter?

    The Jakarta EE platform is the successor to Java EE, maintained by the Eclipse Foundation.

    After a transition from Oracle to Eclipse, the package names were changed to “jakarta.servlet” to avoid conflicts with the “javax.servlet” packages used in Java EE.

    Jakarta filters are implemented using the jakarta.servlet.Filter interface, which is similar to the javax.servlet.Filter interface used in Java EE.

    The purpose and functionality of Jakarta filters remain the same as Java EE filters.

    What are the differences?

    The main difference between Java EE and Jakarta EE are the package names.

    Jakarta filters are used in web applications that are built using Jakarta EE specifications and frameworks.

    They provide the same capabilities as Java EE filters. On top of it, as time goes on, Jakarta EE is aligned with the latest standards and updates introduced by the Jakarta EE community.

    The industry trend is moving towards using Jakarta EE filters for new projects and migrating existing Java EE applications to Jakarta EE.

    Code examples

    I’m going to create 2 different filters. One using Java EE API and the other using Jakarta EE API. First, I create the test and then the implementation.

    Both filters do the same thing. Denying access to a resource when the request remote address is the localhost. This is for demonstration purposes only. You can do a lot more with filters.

    Java EE filter example – Tests

    package systems.loreto.javaee.filters;
    
    import org.junit.jupiter.api.AfterEach;
    import org.junit.jupiter.api.BeforeEach;
    import org.junit.jupiter.api.DisplayName;
    import org.junit.jupiter.api.Test;
    import org.mockito.Mock;
    import org.mockito.MockitoAnnotations;
    
    import javax.servlet.FilterChain;
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    
    import static org.junit.jupiter.api.Assertions.fail;
    import static org.mockito.Mockito.*;
    
    class JavaEEInvalidIPsFilterTest {
    
        @Mock
        private HttpServletRequest httpRequest;
    
        @Mock
        private HttpServletResponse httpResponse;
    
        @Mock
        private FilterChain filterChain;
    
        private JavaEEInvalidIPsFilter javaEEInvalidIPsFilter;
    
        private AutoCloseable openMocks;
    
        @BeforeEach
        void setup() {
            openMocks = MockitoAnnotations.openMocks(this);
            javaEEInvalidIPsFilter = new JavaEEInvalidIPsFilter();
        }
    
        @AfterEach
        void tearDown() {
            try {
                openMocks.close();
            } catch (Exception e) {
                fail("FAILED to close the open mocks.");
            }
        }
    
        @Test
        @DisplayName("Block requests from specific IP")
        void blockRequestFromSpecificIP() throws IOException, ServletException {
            when(httpRequest.getRemoteAddr()).thenReturn("127.0.0.1");
    
            javaEEInvalidIPsFilter.doFilter(httpRequest, httpResponse, filterChain);
    
            verify(httpResponse).sendError(HttpServletResponse.SC_FORBIDDEN, "Access denied");
    
            verifyNoInteractions(filterChain);
        }
    
        @Test
        @DisplayName("Allow requests from different IPs")
        void allowRequestFromDifferentIP() throws IOException, ServletException {
            when(httpRequest.getRemoteAddr()).thenReturn("192.168.0.1");
    
            javaEEInvalidIPsFilter.doFilter(httpRequest, httpResponse, filterChain);
    
            verify(filterChain).doFilter(httpRequest, httpResponse);
    
            verifyNoInteractions(httpResponse);
        }
    }
    
    

    Java EE filter example – Implementation

    package systems.loreto.javaee.filters;
    
    import javax.servlet.*;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    
    public class JavaEEInvalidIPsFilter implements Filter {
    
        private static final String LOCAL_HOST_IP = "127.0.0.1";
    
        private static boolean isLocalHost(HttpServletRequest httpRequest) {
            var ipAddress = httpRequest.getRemoteAddr();
            return LOCAL_HOST_IP.equals(ipAddress);
        }
    
        @Override
        public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
                throws IOException, ServletException {
            var httpRequest = (HttpServletRequest) request;
            var httpResponse = (HttpServletResponse) response;
    
            if (isLocalHost(httpRequest)) {
                httpResponse.sendError(HttpServletResponse.SC_FORBIDDEN, "Access denied");
                return;
            }
    
            chain.doFilter(request, response);
        }
    }
    
    

    Jakarta EE filter example – Tests

    package systems.loreto.jakartaee.filters;
    
    import jakarta.servlet.FilterChain;
    import jakarta.servlet.ServletException;
    import jakarta.servlet.http.HttpServletRequest;
    import jakarta.servlet.http.HttpServletResponse;
    import org.junit.jupiter.api.AfterEach;
    import org.junit.jupiter.api.BeforeEach;
    import org.junit.jupiter.api.DisplayName;
    import org.junit.jupiter.api.Test;
    import org.mockito.Mock;
    import org.mockito.MockitoAnnotations;
    
    import java.io.IOException;
    
    import static org.junit.jupiter.api.Assertions.fail;
    import static org.mockito.Mockito.*;
    
    class JakartaEEInvalidIPsFilterTest {
    
        @Mock
        private HttpServletRequest httpRequest;
    
        @Mock
        private HttpServletResponse httpResponse;
    
        @Mock
        private FilterChain filterChain;
    
        private JakartaEEInvalidIPsFilter jakartaEEInvalidIPsFilter;
    
        private AutoCloseable openMocks;
    
        @BeforeEach
        void setup() {
            openMocks = MockitoAnnotations.openMocks(this);
            jakartaEEInvalidIPsFilter = new JakartaEEInvalidIPsFilter();
        }
    
        @AfterEach
        void tearDown() {
            try {
                openMocks.close();
            } catch (Exception e) {
                fail("FAILED to close the open mocks.");
            }
        }
    
        @Test
        @DisplayName("Block requests from specific IP")
        void blockRequestFromSpecificIP() throws IOException, ServletException {
            when(httpRequest.getRemoteAddr()).thenReturn("127.0.0.1");
    
            jakartaEEInvalidIPsFilter.doFilter(httpRequest, httpResponse, filterChain);
    
            verify(httpResponse).sendError(HttpServletResponse.SC_FORBIDDEN, "Access denied");
    
            verifyNoInteractions(filterChain);
        }
    
        @Test
        @DisplayName("Allow requests from different IPs")
        void allowRequestFromDifferentIP() throws IOException, ServletException {
            when(httpRequest.getRemoteAddr()).thenReturn("192.168.0.1");
    
            jakartaEEInvalidIPsFilter.doFilter(httpRequest, httpResponse, filterChain);
    
            verify(filterChain).doFilter(httpRequest, httpResponse);
    
            verifyNoInteractions(httpResponse);
        }
    }
    
    

    Jakarta EE filter example – Implementation

    package systems.loreto.jakartaee.filters;
    
    import jakarta.servlet.*;
    import jakarta.servlet.http.HttpServletRequest;
    import jakarta.servlet.http.HttpServletResponse;
    
    import java.io.IOException;
    
    public class JakartaEEInvalidIPsFilter implements Filter {
    
        private static final String LOCAL_HOST_IP = "127.0.0.1";
    
        private static boolean isLocalHost(HttpServletRequest httpRequest) {
            var ipAddress = httpRequest.getRemoteAddr();
            return LOCAL_HOST_IP.equals(ipAddress);
        }
    
        @Override
        public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
                throws IOException, ServletException {
            var httpRequest = (HttpServletRequest) request;
            var httpResponse = (HttpServletResponse) response;
    
            if (isLocalHost(httpRequest)) {
                httpResponse.sendError(HttpServletResponse.SC_FORBIDDEN, "Access denied");
                return;
            }
    
            chain.doFilter(request, response);
        }
    }
    
    

    Conclusion

    As we can see, the tests are the same apart from the class being tested and the packages which are being imported. One is from javax and the other is from jakarta.

    The same goes for the implementation.

    This code has been shared in github.

  • What are Java LTS (Long-Term Support) versions and non-LTS versions

    Java LTS (Long-Term Support) versions

    These versions provide stability, reliability, and long-lasting support for enterprise and production environments.

    They undergo extensive testing, receive regular updates, bug fixes, and security patches for an extended period. Usually five years or more.

    LTS versions offer a more robust and secure development platform while ensuring compatibility with third-party libraries and providing a solid foundation for long-term projects.

    Java non-LTS (Non-Long-Term Support) versions

    Introduce the latest features, enhancements, and improvements to the Java platform.

    These releases occur between LTS versions and allow developers to experiment, prototype, and stay up-to-date with the evolving Java technology landscape.

    Non-LTS versions provide a platform for leveraging cutting-edge advancements in the Java ecosystem but may have shorter support life-cycles. Usually around 6 months to 1 year.

    They require more frequent updates and migration efforts, considering the shorter support duration.

    Conclusion

    The choice between Java LTS and non-LTS versions depends on project requirements and priorities.

    LTS versions are recommended for stability, backward compatibility, and compatibility with third-party libraries. They are ideal for long-term projects that require reliable and well-supported Java environments.

    Non-LTS versions, while providing access to the latest features and advancements, are better suited for exploring new technologies, prototyping, and staying ahead of the curve.

    Developers and development teams should carefully evaluate the trade-offs between stability and innovation when deciding which version to adopt for their specific projects.

  • Java 8 Features

    Java 8 introduced some desired features that significantly enhanced the Java programming language.

    The code examples in this blog are available on GitHub.

    In this article, we’ll dive into some of these key features.

    Bear in mind, each feature has a lot more into it than what I go into in this article.

    Here are some of the Java 8 features we’ll talk about in this articles:

    • Lambda Expressions
    • Stream API
    • Default Methods
    • Functional Interfaces
    • Method References
    • Optional Class
    • Date and Time API
    • Java I/O Improvements
    • forEach() Method
    • Collections API Improvements

    Lambda Expressions

    Enable functional programming by allowing the representation of anonymous functions as method arguments.

    They provide a concise syntax for writing inline functions, making code more expressive and readable.

    In the example below, we have a test method named calculate_area that demonstrates the usage of lambda expressions.

    The lambda expression is defined using a functional interface named ShapeFunctionalInterface, which has a single abstract method.
    The lambda expression multiplies the given number by itself to calculate_area of the square.

    package systems.loreto.java8;
    
    import org.junit.jupiter.api.Test;
    
    import static org.junit.jupiter.api.Assertions.assertEquals;
    
    class ShapeLambdaTest {
    
        @Test
        void calculate_area() {
            ShapeFunctionalInterface square = (int number) -> number * number;
    
            int result = square.calculate(3);
    
            assertEquals(9, result);
        }
    
        @FunctionalInterface
        interface ShapeFunctionalInterface {
            int calculate(int number);
        }
    }
    

    Stream API

    The Stream API introduced a new way of working with collections and performing operations on data in a declarative and functional style.

    Streams provide powerful methods for filtering, mapping, reducing, and parallel processing of data.

    Below, we have a test method named square_numbers_from_a_list that demonstrates the usage of Stream API.

    We start by creating a list of integers named numbers. Then, using the stream() method, we obtain a stream of elements from the list. We chain the map() operation to square each element of the stream, and finally, we collect the squared numbers into a new list using the collect() method.

    package systems.loreto.java8;
    
    import org.junit.jupiter.api.Test;
    
    import java.util.Arrays;
    import java.util.List;
    import java.util.stream.Collectors;
    
    import static org.junit.jupiter.api.Assertions.assertEquals;
    
    class StreamApiTest {
    
        @Test
        void square_numbers_from_a_list() {
            List<Integer> numbers =
                    Arrays.asList(1, 2, 3, 4, 5, 6);
    
            List<Integer> squaredNumbers = numbers.stream()
                    .map(number -> number * number)
                    .collect(Collectors.toList());
    
            assertEquals(
                    Arrays.asList(1, 4, 9, 16, 25, 36),
                    squaredNumbers);
        }
    }
    
    

    Default Methods

    Java 8 introduced the concept of default methods in interfaces, enabling interfaces to have concrete method implementations.

    This feature allows for backward compatibility when adding new methods to existing interfaces without breaking the implementing classes.

    We have an example below where we have an interface named Calculator with two methods: add() and subtract().

    The add() method is not a default method, so it must be implemented in the classes that implement the interface. On the other hand, the subtract() method is a default method that provides a default implementation in the interface itself.

    We then have a class named BasicCalculator that implements the Calculator interface and provides an implementation for the add() method. Since the subtract() method has a default implementation in the interface, there’s no need to explicitly implement it in the class.

    In the functionality() test method, we create an instance of BasicCalculator and test both the add() and subtract() methods.

    package systems.loreto.java8.defaultMethods;
    
    import org.junit.jupiter.api.Test;
    import static org.junit.jupiter.api.Assertions.assertEquals;
    
    class BasicCalculatorTest {
    
        @Test
        public void functionality() {
            BasicCalculator calculator = new BasicCalculator();
    
            int sum = calculator.add(4, 3);
            assertEquals(7, sum);
    
            int difference = calculator.subtract(8, 3);
            assertEquals(5, difference);
        }
    }
    
    package systems.loreto.java8.defaultMethods;
    
    interface Calculator {
        int add(int a, int b);
    
        default int subtract(int a, int b) {
            return a - b;
        }
    }
    
    package systems.loreto.java8.defaultMethods;
    
    class BasicCalculator implements Calculator {
        @Override
        public int add(int a, int b) {
            return a + b;
        }
    }
    

    Functional Interfaces

    These are interfaces that have a single abstract method.

    Java 8 introduced the @FunctionalInterface annotation to explicitly mark interfaces as functional interfaces. This enables better support for lambda expressions and method references.

    Below, we have a functional interface named CalculatorOperation with a single abstract method calculate(), representing a calculator operation. We then have a Calculator class that has a performOperation() method, which takes two operands and a CalculatorOperation instance to perform the calculation.

    In the testCalculatorFunctionality() test method, we create an instance of Calculator and test two different operations.

    Firstly, we test addition using a lambda expression as the CalculatorOperation to perform the calculation. We pass in the lambda expression (a, b) -> a + b to calculate the sum of two operands.

    Secondly, we test subtraction using a lambda expression assigned to a variable of type CalculatorOperation, and then pass that variable to the performOperation() method.

    package systems.loreto.java8.functionalInterfaces;
    
    import org.junit.jupiter.api.Test;
    
    import static org.junit.jupiter.api.Assertions.assertEquals;
    
    public class CalculatorTest {
    
        @Test
        public void testCalculatorFunctionality() {
            Calculator calculator = new Calculator();
            int sum = calculator.performOperation(5, 3, (a, b) -> a + b);
            assertEquals(8, sum);
    
            CalculatorOperation subtraction = (a, b) -> a - b;
            int difference = calculator.performOperation(10, 4, subtraction);
            assertEquals(6, difference);
        }
    }
    
    package systems.loreto.java8.functionalInterfaces;
    
    class Calculator {
        public int performOperation(int a, int b, CalculatorOperation operation) {
            return operation.calculate(a, b);
        }
    }
    
    @FunctionalInterface
    interface CalculatorOperation {
        int calculate(int a, int b);
    }
    

    Method References

    Method references provide a shorthand syntax for referring to methods by their names.

    They allow developers to pass methods as arguments or assign them to functional interfaces without explicitly writing lambda expressions.

    We have a Calculator class with a static method add() that performs addition of two integers. We then have a CalculatorTest class where we define a test method named functionality().

    Within the test method, we create a BiFunction named addition that takes two integers as input and uses the method reference Calculator::add to reference the add() method of the Calculator class. This method reference allows us to use the add() method as a functional interface implementation.

    We then use the apply() method of the BiFunction to pass in the operands 5 and 3, which results in the addition operation being performed using the add() method.

    package systems.loreto.java8.methodReferences;
    
    import org.junit.jupiter.api.Test;
    import java.util.function.BiFunction;
    
    import static org.junit.jupiter.api.Assertions.assertEquals;
    
    public class CalculatorTest {
    
        @Test
        public void functionality() {
            BiFunction<Integer, Integer, Integer> addition = Calculator::add;
            int sum = addition.apply(5, 3);
            assertEquals(8, sum);
        }
    }
    
    package systems.loreto.java8.methodReferences;
    
    class Calculator {
    
        public static int add(int a, int b) {
            return a + b;
        }
    }
    

    Optional Class

    The Optional class is a container object that may or may not contain a non-null value.

    It helps in handling null values and reduces the risk of NullPointerExceptions or others, as the context may vary, by enforcing explicit checks and handling for potential null values.

    In the example below, we have a Calculator class with a divide() method that is responsible for performing the division operation and returning an Optional<Integer>.

    In the CalculatorTest class, we create an instance of Calculator in each test method and invoke the divide() method accordingly.

    On divide_valid_input() method, we test valid inputs. In this case 10 and 2, are divisable. Then, an Optional<Integer> is returned with the correct value.

    On divide_by_zero() method, we test when we are dividing an integer by zero, we return an Optional.empty() avoiding an ArithmeticException.

    package systems.loreto.java8.optionalClass;
    
    import static org.junit.jupiter.api.Assertions.assertEquals;
    import static org.junit.jupiter.api.Assertions.assertTrue;
    import static org.junit.jupiter.api.Assertions.assertFalse;
    
    import org.junit.jupiter.api.Test;
    
    import java.util.Optional;
    
    class CalculatorTest {
    
        @Test
        public void divide_valid_input() {
            Calculator calculator = new Calculator();
            Optional<Integer> result = calculator.divide(10, 2);
    
            assertTrue(result.isPresent());
            assertEquals(5, result.get());
        }
    
        @Test
        public void divide_by_zero() {
            Calculator calculator = new Calculator();
            Optional<Integer> result = calculator.divide(10, 0);
    
            assertFalse(result.isPresent());
        }
    }
    
    package systems.loreto.java8.optionalClass;
    
    import java.util.Optional;
    
    class Calculator {
    
        public Optional<Integer> divide(int a, int b) {
            return (b == 0)
                    ? Optional.empty()
                    : Optional.of(a / b);
        }
    }
    

    Date and Time API

    Java 8 introduced the java.time package, which provides a comprehensive set of classes for working with dates, times, durations, and time zones. The new Date and Time API address the limitations and design flaws of the legacy java.util.Date and java.util.Calendar classes.

    Below, we have two test methods within the DateAndTimeApiTest class.

    package systems.loreto.java8.dateAndTimeApi;
    
    import static org.junit.jupiter.api.Assertions.assertEquals;
    
    import org.junit.jupiter.api.Test;
    
    import java.time.LocalDate;
    import java.time.Month;
    import java.time.Period;
    
    public class DateAndTimeApiTest {
    
        @Test
        public void period_between() {
            LocalDate startDate = LocalDate.of(2023, Month.JANUARY, 1);
            LocalDate endDate = LocalDate.of(2023, Month.JUNE, 3);
    
            Period period = Period.between(startDate, endDate);
    
            assertEquals(5, period.getMonths());
            assertEquals(0, period.getYears());
            assertEquals(2, period.getDays());
        }
    
        @Test
        public void local_date_manipulation() {
            LocalDate currentDate = LocalDate.of(2023, Month.JANUARY, 1);
    
            LocalDate nextMonth = currentDate.plusMonths(1);
            LocalDate nextYear = currentDate.plusYears(1);
            LocalDate previousWeek = currentDate.minusWeeks(1);
    
            assertEquals(2, nextMonth.getMonthValue());
            assertEquals(2024, nextYear.getYear());
            assertEquals(2022, previousWeek.getYear());
        }
    }
    

    The first method, period_between(), demonstrates the usage of the Period.between() method to calculate the period between two LocalDate objects.

    We create a start date of January 1, 2023, and an end date of June 3, 2023. By calling Period.between(startDate, endDate), we obtain a Period object representing the difference between the two dates.

    The assertions then verify that the period consists of 5 months, 0 years, and 2 days.

    The second method, local_date_manipulation(), showcases the manipulation capabilities of LocalDate.

    We start with a current date of January 1, 2023, and use the plusMonths(), plusYears(), and minusWeeks() methods to calculate different dates.

    The assertions confirm that the resulting dates match the expected values.

    In these 2 tests, we can see the usage of some of the Date and Time API features introduced in Java 8.

    Java I/O Improvements

    Java 8 introduced several enhancements to the Java I/O API, including new classes like Files and Paths for improved file handling and path manipulation. It also introduced the BufferedReader.lines() method, which allows reading lines from a file in a more streamlined manner.

    In the test class below, we count the lines in the file we created.

    package systems.loreto.java8.ioImprovements;
    
    import org.junit.jupiter.api.AfterAll;
    import org.junit.jupiter.api.Test;
    
    import java.io.IOException;
    import java.nio.file.Files;
    import java.nio.file.Path;
    import java.util.stream.Stream;
    
    import static org.junit.jupiter.api.Assertions.assertEquals;
    
    class IOImprovementsTest {
    
        private static final String TEST_TXT = "src/test/resources/test.txt";
    
        @Test
        void read_lines_from_file() throws IOException {
            Path filePath = Path.of(TEST_TXT);
            Files.write(filePath, "Hello\nWorld\nJava 8".getBytes());
    
            Stream<String> lines = Files.lines(filePath);
            long count = lines.count();
    
            assertEquals(3, count);
        }
    
        @AfterAll
        static void delete_files_create_for_test_purposes() throws IOException {
           Files.delete(Path.of(TEST_TXT));
        }
    }
    

    First, we create a Path object representing the file path where we want to write our test data. We then use the Files.write() method to write the content “Hello\nWorld\nJava 8” to the file.

    Next, we use a Java I/O improvement feature by calling the Files.lines() method, which returns a stream of lines from the specified file. We assign this stream to the lines variable.

    To validate the functionality, we use the count() method on the lines stream to count the number of lines in the file. The assertion checks that the count is equal to the expected value of 3.

    We then proceed to delete the file created for this test by having a delete_files_create_for_test_purposes() static method with the @AfterAll annotations.

    As most of us are familiar with this annotations, this method will be called by junit after all the tests have been executed in this test class.

    forEach() Method

    The forEach() method was added to the Iterable interface, allowing for more concise and expressive iteration over collections. It provides a convenient way to perform an action for each element in a collection without the need for explicit iteration.

    In this example, we have a test method called for_each() within the ForEachMethodTest class.

    package systems.loreto.java8.forEachMethod;
    
    import org.junit.jupiter.api.Test;
    
    import java.util.ArrayList;
    import java.util.List;
    
    import static org.junit.jupiter.api.Assertions.assertEquals;
    
    public class ForEachMethodTest {
    
        @Test
        public void for_each() {
            List<String> fruits = new ArrayList<>();
            fruits.add("Apple");
            fruits.add("Banana");
            fruits.add("Orange");
    
            StringBuilder result = new StringBuilder();
    
            fruits.forEach(fruit -> result.append(fruit).append(" "));
    
            assertEquals("Apple Banana Orange ", result.toString());
        }
    }
    

    First, we create an ArrayList called fruits and add three fruit names to it: “Apple”, “Banana”, and “Orange”.

    Next, we create a StringBuilder called result to store the concatenated fruit names.

    Using the forEach() method introduced in Java 8, we iterate over each element in the fruits list. The lambda expression

    fruit -> result.append(fruit).append(" ")
    

    is used as the action to be performed for each element. It appends the current fruit name followed by a space to the result StringBuilder.

    Finally, we assert that the final value of the result StringBuilder is equal to the expected value “Apple Banana Orange “. This verifies that the forEach() method correctly iterated over the list and executed the provided action on each element.

    Collection API Improvements

    Java 8 introduced several enhancements to the Collection API.

    Notable additions include the stream() and parallelStream() methods, which allow performing functional-style operations on collections.

    The List interface gained the sort() method for in-place sorting, and the Map interface gained the computeIfAbsent() and computeIfPresent() methods for more efficient handling of key-value mappings.

    In the CollectionAPIImprovementsTest class below, we have three test methods. Each one demonstrates a different aspect of the Collections API improvements introduced in Java 8.

    package systems.loreto.java8;
    
    import org.junit.jupiter.api.Test;
    
    import java.util.ArrayList;
    import java.util.HashMap;
    import java.util.List;
    import java.util.Map;
    import java.util.stream.Collectors;
    
    import static org.junit.jupiter.api.Assertions.assertEquals;
    
    class CollectionAPIImprovementsTest {
    
        @Test
        public void stream_and_parallel_stream_methods() {
            List<String> fruits = new ArrayList<>();
            fruits.add("Apple");
            fruits.add("Banana");
            fruits.add("Orange");
    
            List<String> upperCaseFruits = fruits.stream()
                    .map(String::toUpperCase)
                    .collect(Collectors.toList());
    
            assertEquals("APPLE", upperCaseFruits.get(0));
            assertEquals("BANANA", upperCaseFruits.get(1));
            assertEquals("ORANGE", upperCaseFruits.get(2));
        }
    
        @Test
        public void sort_method() {
            List<String> fruits = new ArrayList<>();
            fruits.add("Banana");
            fruits.add("Orange");
            fruits.add("Apple");
    
            fruits.sort(String::compareToIgnoreCase);
    
            assertEquals("Apple", fruits.get(0));
            assertEquals("Banana", fruits.get(1));
            assertEquals("Orange", fruits.get(2));
        }
    
        @Test
        public void compute_if_absent_and_compute_if_present_methods() {
            Map<String, Integer> scores = new HashMap<>();
            scores.put("John", 90);
    
            scores.computeIfAbsent("Alice", key -> 80);
            scores.computeIfPresent("John", (key, value) -> value + 10);
    
            assertEquals(100, scores.get("John"));
            assertEquals(80, scores.get("Alice"));
        }
    }
    

    The stream_and_parallel_stream_methods() method demonstrates the usage of the stream() method on a List. It creates a list of fruits, converts each fruit to uppercase using the map() operation, and collects the results into a new list. The assertions validate that the uppercase fruits are obtained correctly.

    The sort_method() method showcases the sort() method introduced in Java 8. It adds three fruits to a list in a random order and then sorts them using the sort() method with a case-insensitive comparator. The assertions ensure that the fruits are sorted correctly.

    The compute_if_absent_and_compute_if_present_methods() method shows the use of the computeIfAbsent() and computeIfPresent() methods on a Map. It creates a map of scores, adds an entry for “John” with a score of 90, and then applies transformations to the values using these methods. The assertions verify that the values are computed correctly.

    Conclusion

    These are some of the most widely used features introduced in Java 8.

    Each of the features mentioned in this article, could easily become article on it’s own.

    This release brought many other enhancements, such as improved annotations, improved type inference, parallel sorting, concurrency and more efficient handling of large amounts of data, to name a few more.

    Java 8 played a crucial role in modernising the Java language and laying the foundation for subsequent Java releases.

  • Java programming language

    Java is a widely-used programming language that has gained immense popularity for its versatility, reliability, and portability. Developed in the mid-1990s by Sun Microsystems (now owned by Oracle Corporation), Java has become one of the most widely adopted programming languages worldwide.

    Platform independence

    One of Java’s standout features is its platform independence as it employs a unique approach where programs are compiled into bytecode. This can then be executed on any platform equipped with a Java Virtual Machine (JVM).

    This “write once, run anywhere” capability has been instrumental in making Java a favourite among developers across various operating systems and devices.

    Object-oriented programming paradigm

    Java places a strong emphasis on objects and classes for code organisation and structure. This approach in Java, enables the creation of modular and reusable code, facilitating easier maintenance and expansion of software projects.

    The language itself draws inspiration from C and C++. Earlier on Java’s life, programmers familiar with these languages would find Java’s syntax relatively straightforward. However, Java incorporates additional features that enhance security, such as built-in memory management and effective exception handling mechanisms.

    Built-in libraries

    Java boasts an extensive standard library that provides a wealth of pre-built classes and methods for common programming tasks. This inclusive library covers a wide range of functionalities, including input/output operations, networking, database connectivity, and graphical user interface (GUI) development. These resources allows developers to save time and effort while developing applications.

    Concurrency

    Another vital aspect of Java. It offers native support for multithreading, allowing for concurrent execution of multiple tasks. This feature is particularly advantageous for applications that require efficient handling of simultaneous operations, such as web servers or data processing systems.

    The Java Community

    Is vibrant and dynamic, featuring a vast ecosystem of frameworks, libraries, and resources. This thriving community encourages collaboration, knowledge-sharing, and continuous improvement. Developers can benefit from a plethora of online forums, extensive documentation, and open-source projects, facilitating learning and growth in the Java programming domain.

    Multiple domains

    Java can be used across multiple domains, including web development, mobile app development (especially with Android), enterprise software, scientific research, and game development. Its versatility and scalability make it suitable for projects of all sizes, ranging from small-scale applications to large enterprise systems.

    Summary

    Java is a versatile programming language known for its platform independence, object-oriented approach, and comprehensive standard library. Its widespread adoption, robust community support, and cross-platform compatibility have made it a popular choice among developers worldwide. Whether one is a beginner or an experienced programmer, either learning it a s first language or as a new language, Java offers a solid foundation for building a wide range of software applications.