Practical Design Patterns for Java Developers PDF (2023)
Document Details
Uploaded by LightHeartedAstronomy9835
St Joseph's College of Engineering
2023
Miroslav Wengner
Tags
Summary
This book, "Practical Design Patterns for Java Developers", by Miroslav Wengner, provides in-depth explanations of design patterns with practical Java examples using the latest Java features. The book covers the essential concepts of object-oriented programming and how to utilize design patterns to build high-quality software.
Full Transcript
Practical Design Patterns for Java Developers Hone your software design skills by implementing popular design patterns in Java Miroslav Wengner BIRMINGHAM—MUMBAI Practical Design Patterns for Java Developers Copyright © 2023 Packt Publishing All rights reserved. No part of this book may be repr...
Practical Design Patterns for Java Developers Hone your software design skills by implementing popular design patterns in Java Miroslav Wengner BIRMINGHAM—MUMBAI Practical Design Patterns for Java Developers Copyright © 2023 Packt Publishing All rights reserved. No part of this book may be reproduced, stored in a retrieval system, or transmitted in any form or by any means, without the prior written permission of the publisher, except in the case of brief quotations embedded in critical articles or reviews. Every effort has been made in the preparation of this book to ensure the accuracy of the information presented. However, the information contained in this book is sold without warranty, either express or implied. Neither the author, nor Packt Publishing or its dealers and distributors, will be held liable for any damages caused or alleged to have been caused directly or indirectly by this book. Packt Publishing has endeavored to provide trademark information about all of the companies and products mentioned in this book by the appropriate use of capitals. However, Packt Publishing cannot guarantee the accuracy of this information. Group Product Manager: Gebin George Publishing Product Manager: Kunal Sawant Senior Editor: Rohit Singh Technical Editor: Maran Fernandes Copy Editor: Safis Editing Project Coordinator: Prajakta Naik Proofreader: Safis Editing Indexer: Subalakshmi Govindhan Production Designer: Prashant Ghare Marketing Coordinator: Sonia Chauhan First published: February 2023 Production reference: 1130123 Published by Packt Publishing Ltd. Livery Place 35 Livery Street Birmingham B3 2PB, UK. ISBN 978-1-80461-467-9 www.packtpub.com I remember the days when I started exploring the Java platform at Sun Microsystems. It became not only a daily job but also a lifelong hobby and passion. This book is dedicated to my incredible wife, Tanja, my wonderful children, Maxi and Elli, and my entire family for giving me the energy, inspiration, and motivation to keep up the strength needed to complete this book. – Miroslav Wengner Foreword It was November 2021. In a conversation about his developer career, a senior Java developer told me: “I’ve been a senior engineer for almost 20 years. I don’t know what to do to get beyond that level.” That is a typical statement. The length of time varies, but far too many developers and engineers feel stuck in a senior position. Don’t get me wrong, it’s not bad to be a senior! You may be working on great projects and using cool technologies. You are probably involved in solving sophisticated problems and facing deep technical challenges. But there may come a day that you, as the aforementioned developer, feel like you could be doing more in your career, could have more influence in the direction of projects, and have independence and autonomy in your work, or maybe you would feel more fulfilled by inspiring and mentoring other developers. Although this is something I have heard many times before, that particular instance got me thinking. You see, earlier that same day, I was looking at the results of the Java Community Process (JCP) election. Miroslav Wengner, my friend Miro, had just been elected to the Executive Committee of the premiere standard body of Java technology. It was impossible not to think about the parallels... I first met Miro when we worked together on the NetBeans team at Sun Microsystems, the company that created Java. Like the aforementioned developer, Miro had also worked with Java for many years. By the time I had a long conversation with him about his developer career, Miro too had been a senior engineer for a number of years. But he didn’t stop there. Now, just a few years later, Miro has gone way beyond senior: freelancer, speaker, open source developer, committer of the Java Mission Control project at OpenJDK, Java Champion, and JCP EC Member, and why not a book author! Like Miro, you don’t have to be stuck at the senior level. You too can build your technical reputation, grow beyond senior positions, and have a larger impact on your project, company, and even the world. All that leads us to this book that you hold in your hands. What attracted me to Miro’s book is the importance of design patterns in achieving exactly this goal: growing beyond the senior level. Whether you want to become a manager, continue your technical career as a staff-plus engineer, or even if you prefer to be more independent and grow as a freelancer or entrepreneur, you will need to take on more responsibilities and go beyond the code. Although design patterns are directly connected to code, they are also separate and independent. They encapsulate proven solutions to the common challenges of designing quality software. Design patterns hover above a specific piece of code or even a specific project, and create a common vocabulary for explaining problems and solutions. Anyone in the business of building software should master this vocabulary to understand, communicate, and participate in discussions. After all, growing beyond senior is taking responsibility for your career. You start as a senior in your project, and then you expand your influence by getting involved with more impactful things. When you master the vocabulary of design patterns, you are more equipped to go beyond your immediate project and get involved in other projects across your company and the industry. You’ll maybe even get to the point of helping define the directions of technology through open source projects and foundations or through standards organizations, like Miro is doing inside the JCP. This book will help you do exactly that. With a broad view of design patterns applied to the Java ecosystem, you will see not only the concepts and the vocabulary, but also the real implementation and impact of design patterns in software that you use daily. And one of the things that always impresses me is Miro’s ability to connect software with real things. His open source project, Robo4J, which even won a Duke’s Choice Award, helps you turn your Java code into robots and drones capable of navigating the real world. I was delighted to see Miro taking the same approach to design patterns. The examples in this book, using multiple vehicles and their parts, are written using the latest Java 17+ features, bringing the designs to life and connecting them to real, concrete problems. So, get started on your journey of building your reputation, growing your career, and reaching beyond the senior level. Knowing design patterns will help you fit right in, speak the language, and participate in the most important decisions. Let the amazing Miro be your guide, and feel free to reach out to me if you would like (as Miro once did) to talk about the next steps in your career. Bruno Souza Principal Consultant, Java Champion, JCP Executive Committee Member @brjavaman https://java.mn Contributors About the author Miroslav Wengner is an engineer with a passion for resilient distributed systems and product quality. He is a co-author and contributor to the Robo4J project (a reactive soft real-time framework for robotics/IoT). Miro contributes to OpenJDK and participates in other open source technologies. He uses his passion for helping build resilient and scalable solutions. Miro was selected for the Java Champions Program, recognized as a JavaOne Rockstar, and elected to the Java Community Process (JCP) as an executive committee member. In addition to his day-to-day duties as a principal engineer at OpenValue, he shares his knowledge at conferences (JavaOne, Devoxx, and so on) and in blogs. Miro believes in the Java ecosystem and helps move it forward! About the reviewer Werner Keil works in areas such as Agile, BDD, Cloud-native DevOps, Java, Java EE/Jakarta EE, IoT, security, and microservices, helping Global 500 clients across various industries and IT vendors. Having worked for over 30 years as PM, coach, software architect, and consultant for different sectors, Werner is an Eclipse and Apache Committer and a JCP member in JSRs. Werner has won multiple JCP awards, including Member of the Year and Outstanding Spec Lead, and was recognized as Speaker of All Times by Java2Days, a large Java conference held in eastern Europe. He is an Eclipse Babel Language Champion, a project lead of Eclipse UOMo, and a committer member in the Jakarta EE Specification Committee. Table of Contents Prefacexvii Part 1: Design Patterns and Java Platform Functionalities 1 Getting into Software Design Patterns 3 Technical requirements 3 The open-closed principle (OCP) 15 Code – from symbols to program 4 The Liskov Substitution Principle (LSP) – substitutability of classes 16 Examining OOP and APIE 6 The interface segregation principle (ISP) 17 Only exposing what’s required – encapsulation 6 The dependency inversion principle (DIP) 18 Inevitable evolution – inheritance 7 Behavior on demand – polymorphism 7 Significance of design patterns 19 Standard features – abstraction 9 Reviewing what challenges design Gluing parts to APIE 12 patterns solve 20 Understanding the SOLID design Summary22 principles13 Questions22 The single-responsibility principle (SRP) – Further reading 23 the engine is just an engine 14 2 Discovering the Java Platform for Design Patterns 25 Technical requirements 26 Exploring the model and Knocking on Java’s door 26 functionality of the Java platform 27 The JDK 28 x Table of Contents The JRE 29 A quick review of Java features from The JVM 29 11 to 17+ 53 The local variable syntax for lambda Reviewing GC and the Java memory parameters (Java SE 11, model34 JEP-323)53 The JMM 34 Switch expressions (Java SE 14, JEP-361) 54 GC and automatic memory management 36 Text blocks (Java SE 15, JEP-378) 54 Examining the core Java APIs 39 Pattern matching for instanceof (Java SE 16, Primitive data types and wrappers 40 JEP-394)54 Working with the String API 42 Records (Java SE 16, JEP-395) 55 Introducing arrays 44 Sealed classes (Java SE 17, JEP-409) 55 Discovering a collection framework 45 UTF-8 by default (Java SE 18, JEP-400) 56 Math APIs 47 Pattern matching for switch (Java SE 18, Second Preview, JEP-420) 57 Functional programming and Java 47 Understanding Java concurrency 57 Introducing lambdas and functional interfaces 48 From a basic thread to executors 58 Using functional interfaces in lambda expressions49 Executing tasks 59 Getting to grips with the Java Module Summary60 System49 Questions61 Further reading 61 Part 2: Implementing Standard Design Patterns Using Java Programming 3 Working with Creational Design Patterns 65 Technical requirements 66 Creating objects from different It all starts with a class that becomes families using the abstract factory an object 66 pattern71 Creating objects based on input with Motivation71 the factory method pattern 67 Finding it in the JDK 72 Sample code 72 Motivation67 Conclusion74 Finding it in the JDK 68 Sample code 68 Instantiating complex objects with Conclusion71 the builder pattern 75 Table of Contents xi Motivation75 Finding it in the JDK 86 Finding it in the JDK 75 Sample code 86 Sample code 75 Conclusion89 Conclusion78 Initiating objects on demand with Cloning objects with the prototype the lazy initialization pattern 90 pattern78 Motivation90 Motivation78 Finding it in the JDK 90 Finding it in the JDK 78 Sample code 90 Sample code 79 Conclusion93 Conclusion82 Reducing class dependencies with the Ensuring only one instance with the dependency injection pattern 94 singleton pattern 82 Motivation94 Motivation82 Finding it in the JDK 94 Finding it in the JDK 82 Sample code 95 Sample code 82 Conclusion98 Conclusion85 Summary99 Improving performance with the Questions99 object pool pattern 85 Further reading 100 Motivation86 4 Applying Structural Design Patterns 101 Technical requirements 102 Treating objects the same way using Incompatible object collaboration the composite pattern 109 with the adapter pattern 102 Motivation109 Motivation102 Finding it in the JDK 109 Finding it in the JDK 102 Sample code 110 Sample code 102 Conclusion111 Conclusion106 Extending object functionality by Decoupling and developing objects using the decorator pattern 111 independently with the bridge pattern106 Motivation112 Motivation106 Finding it in the JDK 112 Finding it in the JDK 106 Sample code 112 Sample code 106 Conclusion114 Conclusion109 xii Table of Contents Simplifying communication with the Identifying instances using the facade pattern 115 marker pattern 126 Motivation115 Motivation126 Finding it in the JDK 115 Finding it in the JDK 127 Sample code 115 Sample code 127 Conclusion116 Conclusion129 Using conditions to select desired Exploring the concept of modules objects with the filter pattern 117 with the module pattern 129 Motivation117 Motivation130 Finding it in the JDK 117 Finding it in the JDK 130 Sample code 117 Sample code 130 Conclusion120 Conclusion132 Sharing objects across an application Providing a placeholder for an object with the flyweight pattern 120 using the proxy pattern 132 Motivation120 Motivation133 Finding it in the JDK 121 Finding it in the JDK 133 Sample code 121 Sample code 133 Conclusion123 Conclusion135 Handling requests with the front- Discovering multiple inheritance in controller pattern 123 Java with the twin pattern 135 Motivation123 Motivation135 Finding it in the JDK 124 Sample code 135 Sample code 124 Conclusion137 Conclusion126 Summary137 Questions138 Further reading 138 5 Behavioral Design Patterns 139 Technical requirements 140 Conclusion143 Limiting expensive initialization Handling events using the chain of using the caching pattern 140 responsibility pattern 143 Motivation140 Motivation143 Finding it in the JDK 140 Finding it in the JDK 143 Sample code 140 Sample code 144 Table of Contents xiii Conclusion146 Conclusion160 Turning information into action with Keeping all interested parties the command pattern 146 informed using the observer pattern 160 Motivation146 Motivation160 Finding it in the JDK 146 Finding it in the JDK 161 Sample code 146 Sample code 161 Conclusion148 Conclusion162 Giving meaning to the context using Dealing with instance stages by using the interpreter pattern 148 the pipeline pattern 162 Motivation148 Motivation162 Finding it in the JDK 149 Finding it in the JDK 163 Sample code 149 Sample code 163 Conclusion150 Conclusion165 Checking all the elements with the Changing object behavior with the iterator pattern 150 state pattern 165 Motivation151 Motivation165 Finding it in the JDK 151 Finding it in the JDK 165 Sample code 151 Sample code 165 Conclusion153 Conclusion167 Utilizing the mediator pattern for Using the strategy pattern to change information exchange 153 object behavior 167 Motivation153 Motivation167 Finding it in the JDK 154 Finding it in the JDK 167 Sample code 154 Sample code 167 Conclusion155 Conclusion169 Restoring the desired state with the Standardizing processes with the memento pattern 155 template pattern 169 Motivation155 Motivation169 Finding it in the JDK 155 Finding it in the JDK 170 Sample code 156 Sample code 170 Conclusion158 Conclusion172 Avoiding a null pointer exception Executing code based on the object state with the null object pattern 158 type using the visitor pattern 172 Motivation158 Motivation172 Finding it in the JDK 159 Finding it in the JDK 172 Sample code 159 Sample code 172 xiv Table of Contents Conclusion174 Questions175 Summary175 Further reading 176 Part 3: Other Essential Patterns and Anti-Patterns 6 Concurrency Design Patterns 179 Technical requirements 180 Sample code 195 Decoupling a method execution with Conclusion198 an active object pattern 180 Decoupling the execution logic with Motivation180 a producer-consumer pattern 198 Sample code 180 Motivation198 Conclusion183 Sample code 198 Non-blocking tasks using async Conclusion201 method invocation pattern 183 Executing isolated tasks with the Motivation184 scheduler pattern 201 Sample code 184 Motivation201 Conclusion187 Sample code 201 Delay execution until the previous Conclusion206 task is completed with the balking Effective thread utilization using a pattern188 thread-pool pattern 206 Motivation188 Motivation206 Sample code 188 Sample code 207 Conclusion191 Conclusion209 Providing a unique object instance Summary210 with a double-checked locking pattern191 Questions210 Motivation191 Further reading 211 Sample code 191 Conclusion194 Answers211 Using purposeful thread blocking via a read-write lock pattern 194 Motivation195 Table of Contents xv 7 Understanding Common Anti-Patterns 213 Technical requirements 214 Functional decomposition 221 What anti-patterns are and how to Boat anchor 221 identify them 214 Conclusion222 Theoretical principles challenges 214 Understanding software architecture Collecting technical debt as a bottleneck 214 anti-patterns222 Inappropriately squeezing the capabilities of Golden hammer 222 the Java platform 215 Continuous obsolescence 223 Selecting the right tool 218 Input kludge 223 Conclusion of the code smell anti-pattern 219 Working in a minefield 224 Examining typical software anti- Ambiguous viewpoint 224 patterns219 Poltergeists224 Spaghetti code 219 Dead end 224 Cut and paste programming 220 Conclusion225 Blob220 Summary225 Lava flow 221 Further reading 226 Assessments227 Index231 Other Books You May Enjoy 240 Preface The Java language is a tool for communicating with a very rich platform that provides many features ready to serve for application development. This book explores the latest developments in improving language syntax with examples of the most useful design patterns. The book reveals the relationship between features, patterns, and platform efficiency through example implementations. The book explores how theoretical foundations help improve the maintainability, efficiency, and testability of source code. The content helps the reader solve different tasks and provides guidance on how to approach programming challenges using a variety of sustainable and transparent approaches. Who this book is for This book is dedicated to all “hungry” engineers who want to improve their software design skills with new language enhancements and a closer look at the Java platform. What this book covers Chapter 1, Getting into Software Design Patterns, introduces us to the initial foundations of source code design structure and outlines the principles that should be followed to achieve maintainability and readability. Chapter 2, Discovering the Java Platform for Design Patterns, discusses the Java platform, which is a very broad and powerful tool. This chapter exposes the features, functions, and design of the Java platform in more detail to continue building the foundation needed to understand the purpose and value of using a design patterns. Chapter 3, Working with Creational Design Patterns, explores object instantiation, which is a key part of any application. This chapter describes how to approach this challenge while keeping the requirements in mind. Chapter 4, Applying Structural Design Patterns, shows how to create source code that allows for clarity of relationships between required objects. Chapter 5, Behavioral Design Patterns, explores how to create source code that allows objects to communicate and exchange information while maintaining a transparent form. Chapter 6, Concurrency Design Patterns, discusses the Java platform and how it is a concurrent environment by nature. It shows how to harness its power for the designed application’s purposes. xviii Preface Chapter 7, Understanding Common Anti-Patterns, deals with anti-patterns that can be found in any application development cycle. It will help you deal with the root causes and their identification, and suggests possible anti-pattern remedies. To get the most out of this book To execute the instructions in this book, you’ll need the following: Software/hardware covered in the book Operating system requirements Java Development Kit 17+ Windows, macOS, or Linux Recommended IDE VSCode 1.73.1+ Windows, macOS, or Linux A text editor or IDE Windows, macOS, or Linux For this book, installation of Java Development Kit 17+ is required. To verify if it is available on your system, execute the following commands: Windows Command Prompt: java –version Linux or macOS system command line: java –version Expected output: openjdk version "17" 2021-09-14 OpenJDK Runtime Environment (build 17+35-2724) OpenJDK 64-Bit Server VM (build 17+35-2724, mixed mode, sharing) In case you don’t have the JDK installed on your local machine, search for the appropriate instructions for your platform at https://dev.java/learn/getting-started-with-java/ and find the matching JDK version at https://jdk.java.net/archive/. To download and install Visual Studio Code, visit https://code.visualstudio.com/ download. The following page describes and guides the use of the VSCode terminal: https://code. visualstudio.com/docs/terminal/basics. Download the example code files You can download the example code files for this book from GitHub at https://github.com/ PacktPublishing/Practical-Design-Patterns-for-Java-Developers. If there’s an update to the code, it will be updated in the GitHub repository. Preface xix We also have other code bundles from our rich catalog of books and videos available at https:// github.com/PacktPublishing/. Check them out! Download the color images We also provide a PDF file that has color images of the screenshots and diagrams used in this book. You can download it here: https://packt.link/nSLEf. Conventions used There are a number of text conventions used throughout this book. Code in text: Indicates code words in text, database table names, folder names, filenames, file extensions, pathnames, dummy URLs, user input, and Twitter handles. Here is an example: “Let us examine the generalization process in the Vehicle class development.” A block of code is set as follows: public class Vehicle { private boolean moving; public void move(){ this.moving = true; System.out.println("moving..."); } When we wish to draw your attention to a particular part of a code block, the relevant lines or items are set in bold: sealed interface Engine permits ElectricEngine, PetrolEngine { void run(); void tank(); } Any command-line input or output is written as follows: $ mkdir main $ cd main Bold: Indicates a new term, an important word, or words that you see onscreen. For instance, words in menus or dialog boxes appear in bold. Here is an example: “The bytecode is running a Java virtual machine (JVM).” xx Preface Tips or important notes Appear like this. Get in touch Feedback from our readers is always welcome. General feedback: If you have questions about any aspect of this book, email us at customercare@ packtpub.com and mention the book title in the subject of your message. Errata: Although we have taken every care to ensure the accuracy of our content, mistakes do happen. If you have found a mistake in this book, we would be grateful if you would report this to us. Please visit www.packtpub.com/support/errata and fill in the form. Piracy: If you come across any illegal copies of our works in any form on the internet, we would be grateful if you would provide us with the location address or website name. Please contact us at [email protected] with a link to the material. If you are interested in becoming an author: If there is a topic that you have expertise in and you are interested in either writing or contributing to a book, please visit authors.packtpub.com. Share Your Thoughts Once you’ve read Practical Design Patterns for Java Developers, we’d love to hear your thoughts! Please click here to go straight to the Amazon review page for this book and share your feedback. Your review is important to us and the tech community and will help us make sure we’re delivering excellent quality content. Preface xxi Download a free PDF copy of this book Thanks for purchasing this book! Do you like to read on the go but are unable to carry your print books everywhere? Is your eBook purchase not compatible with the device of your choice? Don’t worry, now with every Packt book you get a DRM-free PDF version of that book at no cost. Read anywhere, any place, on any device. Search, copy, and paste code from your favorite technical books directly into your application. The perks don’t stop there, you can get exclusive access to discounts, newsletters, and great free content in your inbox daily Follow these simple steps to get the benefits: 1. Scan the QR code or visit the link below https://packt.link/free-ebook/9781804614679 2. Submit your proof of purchase 3. That’s it! We’ll send your free PDF and other benefits to your email directly Part 1: Design Patterns and Java Platform Functionalities This part covers the purpose of software design patterns. It outlines the fundamental ideas of the object-oriented programming APIE and SOLID design principles and provides an introduction to the Java platform, which is crucial in understanding how to effectively utilize design patterns. This part contains the following chapters: Chapter 1, Getting into Software Design Patterns Chapter 2, Discovering the Java Platform for Design Patterns 1 Getting into Software Design Patterns Every software architect or developer often faces the challenges of structuring code – how to develop a code structure that remains sustainable, just as an artist draws their painting. This chapter will take us on a journey into writing program code. You will explore the challenges behind the structure of code and its organization. Together, we will approach the topic from an early stage described by the pillars of object-oriented programming, known as APIE. We will also review the principles of SOLID to gain clarity in understanding design patterns. In this chapter, we will cover the following topics: Code – from symbols to program Examining OOP and APIE Understanding the SOLID design principles The significance of design patterns Reviewing what challenges design patterns solve By the end of this chapter, you will have reviewed the basic programming concepts, which will form the basis of the rest of the book. Technical requirements You can find the code files for this chapter on GitHub at https://github.com/PacktPublishing/ Practical-Design-Patterns-for-Java-Developers/tree/main/Chapter01. 4 Getting into Software Design Patterns Code – from symbols to program Human speech is fruitful, rich, colorful, and way beyond what the words themselves may express. Nouns, verbs, and adjectives for precisely expressing a moment or action can be used. In contrast, machines do not understand the complex constructions or expressions that humans are able to create. Machine language is limited, well-defined, extremely specific, and simplified. Its goal is to provide the precise expression of intent for which it is designed. This contrasts with human language whose purpose is just communication and not necessarily with specifics. A machine’s intent can be expressed as a defined instruction or a set of them. This means that machines understand the instructions. These instructions must be available to the machine in some form at the time of execution. Each machine normally has a set of instructions. Based on this kind of instruction set, machines can perform the required instructions, as shown here: Figure 1.1 – A simplified instruction cycle inside the CPU (instruction is taken from memory and the result is stored) Let us explore one individual instruction. The instruction can be understood as a command given to the processor. The processor is the heart of the machine, or the center of the ordering and executing of processes. The machine may contain one or more of them. It depends on its design, but in any case, there is always one that takes the lead. For further simplification, we will only consider one – that is, consider a system that only has one central processing unit (CPU) dedicated to executing a program. A CPU is a device that executes instructions containing a computer program. The CPU must contain such an instruction set, as shown in the previous diagram, to process the requested action. Because instructions can take completely different forms depending on the CPU, there is no defined standard. This promotes different CPU platforms, which is not necessarily a bad thing and contributes to evolution. However, the fact remains that the instructions are not easy for people to read. We have stated that machines can perform instruction collection, ideally as a continuous flow. The flow of instructions can be simplified as a queue in memory, where one instruction goes in and the other leaves. The CPU plays the role of an interpreter who works with this memory cyclically (as we saw in Figure 1.1). Okay, so the CPU interprets, but as the instructions are added to the memory, where do they come from, and how can such a stream be created? Code – from symbols to program 5 Let us gather some thoughts. Machine instructions, in most cases, originate from a compiler. What is a compiler? The compiler can be viewed as a CPU or a platform-specific program that translates text into target actions. The text we use to call the program and the result could be named machine code. The following diagram illustrates this: Figure 1.2 – A simplified platform-specific flow from the source code through the compiler program to its resultant action Machine code is a low-level language that the machine understands and consists of language instructions that are processed sequentially (see Figure 1.1); the program was compiled, executed, and run. In the case of Java, there is no machine code: Figure 1.3 – A simplified flow for the Java program through the compiler to its platform execution The source code is compiled by the Java compiler into bytecode. The bytecode is running a Java virtual machine (JVM) (see Figure 1.3). In this situation, the JVM plays the role of the interface between the bytecode and the actual instructions that are executed on the CPU. The JVM emulates a bytecode instruction. It does this using the just-in-time (JIT) compiler that is part of the JVM. The JIT compiler translates bytecode instructions into native processor instructions. The JVM is a platform-specific interpreter, analogous to directly compiled code (see Figure 1.2). The JVM also provides additional features such as memory management and garbage collection, which is what makes the Java platform so powerful. All these features allow developers to write code once, compile it into bytecode, and run a supported platform – known as write once, run anywhere (WORA). In the context of the previous exploration, Java is a high-level language that is translated to a low level. Java provides a strong abstraction from the details of computer functionality. It allows programmers to create simpler programs for complex challenges. At this point, we begin our journey of jointly exploring standardized solutions. Later in the book, we will review how to create code that is maintainable and extensible with fewer memory requirements. Together, we will discuss different types of design patterns that can help us to make our daily work understandable, transparent, and more fun. 6 Getting into Software Design Patterns Examining OOP and APIE In the previous section, we learned how a program written in one of the high-level languages is converted into machine instructions that are processed by the CPU. The high-level language provides a framework for expressing the desired ideas by following the details of the language implementation. Such languages commonly provide many neat constructions or statements that do not limit the imagination. In object-oriented programming (OOP) language, the representation of the core carrier is presented by the concept of the object. This book focuses on the Java language. Java is a fully object-oriented language with additional features. What does object-oriented language mean exactly? In computer science, this means that the program focuses on the concept of classes, where instances of these classes represent an object. Next, we will repeat the importance of the OOP paradigm and deal with some basic concepts. These terms can be expressed by the abbreviation of abstraction, polymorphism, inheritance, and encapsulation (APIE). The letters APIE indicate the four basic pillars of OOP languages. Let’s examine each word in a separate section in reverse order – so, EIPA. The motivation is to bring more clarity to our understanding of the concept of OOP. Only exposing what’s required – encapsulation The first in reverse order is encapsulation – let’s start with it. OOP languages, including Java, work with the concept of classes. Imagine that a class is a vehicle. The class provides all the fields that can be statically typed or object-specific – that is, initiated after an object is instantiated in the allocated memory. The concept is similar with respect to class or object methods. The method may belong to a class or its instance – in the considered example, to a vehicle. Any method can work over an object or class field and change the internal state of the vehicle or the field values (see Example 1.1): public class Vehicle { private boolean moving; public void move(){ this.moving = true; System.out.println("moving..."); } public void stop(){ this.moving = false; System.out.println("stopped..."); } } Example 1.1 – The Vehicle class hides an internal state (moving) Examining OOP and APIE 7 We can apply encapsulation to the example of a vehicle. We imagine a real vehicle – only one. In such an imaginary vehicle, all internal elements and internal functions remain hidden from the driver. It only exposes the functionality it serves, such as the steering wheel, which the driver can control. This is the general principle of encapsulation. The state of an instance can be changed or updated through exposed methods or fields; everything else is hidden from the outside world. It is quite a good practice to use methods to modify the inner array or arrays of an instance. But we will repeat that later in this book. So far, it’s just a good hint. Inevitable evolution – inheritance In the previous section, an instance of an imaginary vehicle class was created. We encapsulated all the functions that should not be exposed to the driver. This means that the driver may not know how the engine works, only how to use it. This section is devoted to the property of inheritance, which we will demonstrate in the following example. Assume that the vehicle’s engine is broken. How can we replace it? The goal is to replace the current one with a functional one. An engine that works this way may not necessarily be the same, especially if the vehicle model already has old parts that are not available on the market. What we do is derived from all the attributes and functions needed to create a new engine. Concerning the class, the new replacement module will be a child in the class hierarchy. Although the engine will not be a perfect replica and does not have the same unique object identifier, it will match all the parent properties. With that, we have described the second pillar of inheritance in OOP – the ability to create a new class above the existing subclass. However, software designers should be wary of the fourth pillar, encapsulation, and any violations caused by a subclass depending on the implementation details of its superclass. Behavior on demand – polymorphism The third concept is polymorphism. With a little imagination, this can be understood as “many forms.” So, what does that mean here? Given the vehicle described previously, it could be defined as the ability to perform a particular action in many ways. This would mean, in the context of a vehicle, that the movement of the other method, move, could happen differently based on the inputs or the state of the instance. Java allows for two types of polymorphism, both of which differ in their runtime behavior. We will discuss both in detail. 8 Getting into Software Design Patterns Method overloading This type is known as static polymorphism. This means that the correct method is resolved during program compilation – so, at compile time. Java provides two types of method overloads: Changing the input argument type: Figure 1.4 – Overloading the method of the Vehicle class by changing the input types Changing the number of method arguments: Figure 1.5 – Overloading the method of the Vehicle class by changing the number of arguments Now, let’s look at the second type of polymorphism. Method overriding This is sometimes called dynamic polymorphism. This means that the method performed is known at runtime. The overridden method is called through reference to the object instance of belongingness. Let us examine a simple example to illustrate this. Consider the Vehicle class a parent class (see Figure 1.6 and Example 1.2) with a method called move: Figure 1.6 – The relation between the overridden move methods for the parent and child classes Examining OOP and APIE 9 We intend to create a child class, Car, with a similar method named move. The child provides slightly different functions because the Car instance moves faster than the parent instance, Vehicle: public class Vehicle { public void move(){ System.out.println("moving..."); } } public class Car extends Vehicle { @Override public void move(){ System.out.println("moving faster."); } } Vehicle vehicle = new Car(); vehicle.move(); output: moving faster... Example 1.2 – The Vehicle variable holds the reference to the Car instance and the appropriate move method is executed at runtime (see Figure 1.6) We will touch on this topic in more detail in Chapter 3, Working with Creational Design Patterns. Standard features – abstraction The last letter to cover (but the first letter in the abbreviation APIE) leads us to the hitherto unspecified pillar of abstraction. The key to this concept is the constant removal of specifics or individual details to achieve the generalization of the purpose of the object. To get the best experience with this concept, let us get into the context with the vehicle example. We do not intend to describe a specific car model that belongs to a group of vehicles. Our goal is to define a common functionality that all types of vehicles under consideration can include in the context of our efforts. With such knowledge, we create a suitable abstraction, an abstract class that can be inherited later when constructing a particular model class (see Example 1.3). This approach allows us to focus our efforts on generalizing and abstracting vehicle characteristics. This can have a positive impact on code reduction and reusability. 10 Getting into Software Design Patterns The abstraction in Java can be achieved in two ways: Abstract classes with abstract methods (see Example 1.3 and Figure 1.7): Figure 1.7 – The AbstractVehicle class with its CommonCar realizations and SportCar classes public abstract class AbstractVehicle { abstract public void move(); public void stop(){ System.out.println("stopped..."); } } public class CommonCar extends AbstractVehicle{ @Override public void move() { System.out.println("move slow..."); } } public class SportCar extends AbstractVehicle{ @Override public void move() { System.out.println("move fast..."); } } Example 1.3 – The extraction of the common functionality without providing a particular implementation by using an abstract class concept Examining OOP and APIE 11 Using interfaces (see Example 1.4 and Figure 1.8) with a generic abstract method: Figure 1.8 – The abstraction concept achieved by using interfaces public interface VehicleInterface { void move(); } public class Truck implements VehicleInterface{ @Override public void move() { System.out.println("truck moves..."); } } public class Bus implements VehicleInterface{ @Override public void move() { System.out.println("bus moves..."); } } Example 1.4 – A similar functionality extraction by using Java interfaces 12 Getting into Software Design Patterns Both concepts of abstraction can be combined (see Figure 1.9): Figure 1.9 – A combination of both abstraction concepts Abstract classes and interfaces have their place in the design of code structure. Their use depends on demand, but both have a very positive impact on code maintainability and help in the use of design patterns. Gluing parts to APIE The motivation for each of the pillars mentioned in the previous sections is to introduce structure into the code through a given set of concepts. The pillars are defined and complementary. Let’s just examine one unit, the Vehicle class, and its instance. Instance logic and data are encapsulated and exposed through methods to the outside world. Vehicle characteristics can be inherited so that a new vehicle design, such as a new model, can be specified. Exposed methods can provide model-based behavior and incoming arguments with internal instance state changes. When crystalizing thoughts about a new vehicle, we can always generalize its behavior and extract it using an abstract class or interface. Let us examine the generalization process over the Vehicle class development. When preparing to define a new vehicle model, we can always generalize its characteristics and extract it using an abstract class or interface. Let’s look at the following diagram: Understanding the SOLID design principles 13 Figure 1.10 – APIE viewed as a continual improvement process Although these four pillars seem trivial, it is incredibly difficult to follow them, as we will continue to show in the following sections and chapters. So far in this section, we learned about the four basic pillars of OOP and examined how these principles affect code design. Next, we will learn more about sustainable code design concepts. Let us roll on to the following section. Understanding the SOLID design principles In the previous sections, the idea of structured work was introduced. The development pillars of APIE were elaborated on in detail using examples. You have gained a foundational understanding of the concept of class instances in terms of object-oriented principles and how we can create different types of specific objects: Figure 1.11 – Vehicle N, where N is a positive integer number, represents an instance of the Vehicle class 14 Getting into Software Design Patterns Classes can be instantiated so that an instance becomes an object. The object must fit into free memory. We say that the object allocates memory space. When Java is considered, allocated memory is virtual space inside the physical system’s memory. Just a small note – we previously discussed the existence of the JVM, an interpreter of compiled bytecode for the required platform (see Figure 1.3). We mentioned other JVM features, one of which is memory management. In other words, the JVM assumes responsibility for allocating virtual memory space. This virtual memory space can be used to allocate an instance of a class. This virtual memory and its fragmentation are taken care of by the JVM and an unused object cleans up the selected garbage collection algorithm, but this is beyond the scope of this book and would be the subject of further study (see Reference 1). Every programmer, although it may not be obvious at first glance, plays the role of a software designer. The programmer creates the code by writing it. The code carries an idea that is semantically transformed into action depending on the text entered. Over time, software development has gone through many phases and many articles have been written and published on software maintenance and reusability. One of the milestones in software development may be considered the year 2000 when Robert C. Martin published his paper on Design Principles and Design Patterns (see Reference 2). The paper reviews and examines techniques in the design and implementation of software development. These techniques were later simplified in 2004 into the mnemonic acronym SOLID. The goal of the SOLID principles is to help software designers make software and its structure more sustainable, reusable, and extensible. In the following sections, we will examine each of the individual terms hidden after the initial letter in the abbreviation SOLID. The single-responsibility principle (SRP) – the engine is just an engine The first principle is a well-defined class goal. We can say that each class should have only one reason to exist. As in, it has the intention and responsibility for only one part of the functionality. The class should encapsulate this part of the program. Let’s put this in the context of an example. Imagine the previous example of a vehicle and its abstraction. We are now extending this class with the Engine and VehicleComputer classes, as shown: Understanding the SOLID design principles 15 Figure 1.12 – The Vehicle class instance using Engine and VehicleComputer realization but an engine functionality does not interfere with the lights The engine can start and stop, but the instance of the Engine class cannot control vehicle lights, for example. The light control is the responsibility of the vehicle computer class instance. The open-closed principle (OCP) This principle states that the class or entity under consideration should be open to extension but closed to modifications. It goes hand in hand with the concepts already mentioned. Let’s put this in the context of an example where we consider the Car and Truck classes. Both classes inherit the Vehicle interface. Both believe that vehicle entities have a move method. By not thinking about proper abstraction and without respecting the OCP, code can easily bear unexpected difficulties when classes are not easy to reuse or cannot be handled (see Example 1.5): public interface Vehicle {} public class Car implements Vehicle{ public void move(){} } public class Truck implements Vehicle { public void move(){} } -- usage -- List vehicles = Arrays.asList(new Truck(), new Car()); vehicles.get(0).move() // ERROR, NOT POSISBLE! Example 1.5 – Although both are considered entities, Truck and Car inherit a Vehicle interface, the move method is compliant, and this causes an issue in extension or execution 16 Getting into Software Design Patterns The correction of the example at hand is very trivial in this case (see Example 1.6): public interface Vehicle { void move(); // CORRECTION! } --- usage --- List vehicles = Arrays.asList(new Truck(), new Car()); vehicles.get(0).move() // CONGRATULATION, ALL WORKS! Example 1.6 – The Vehicle interface provides a move abstraction method Obviously, as code evolves, non-compliance leads to unexpected challenges. The Liskov Substitution Principle (LSP) – substitutability of classes The previous sections dealt with inheritance and abstraction as two of the key pillars of OOP. It will come as no surprise to those of you who have read carefully that, given the class hierarchy of parent- child relationships, a child may be replaced or represented by its parent and vice versa (see Example 1.7). Let us look at the example of CarWash, where you can wash any vehicle: public interface Vehicle { void move(); } public class CarWash { public void wash(Vehicle vehicle){} } public class Car implements Vehicle{ public void move(){} } public class SportCar extends Car {} --- usage --- CarWash carWash = new CarWash(); carWash.wash(new Car()); carWash.wash(new SportCar()); Example 1.7 – A CarWash example where any Vehicle type can be substituted by appro- priate instances of classes in the class hierarchy Understanding the SOLID design principles 17 This means that classes of a similar type can act analogously and replace the original class. This statement was first mentioned during a keynote address by Barbara Liskov in 1988 (see Reference 3). The conference focused on data abstraction and hierarchy. The statement was based on the idea of substitutability of class instances and interface segregation. Let’s look at interface segregation next. The interface segregation principle (ISP) This principle states that no instance of a class should be forced to depend on methods that are not used or in their abstractions. It also provides instructions on how to structure interfaces or abstract classes. In other words, it controls how to divide the intended methods into smaller, more specific entities. The client could use these entities transparently. To point out a malicious implementation, consider Car and Bike as children of the Vehicle interface, which shares all the abstract methods (see Example 1.8): public interface Vehicle { void setMove(boolean moving); boolean engineOn(); boolean pedalsMove(); } public class Bike implements Vehicle{ ... public boolean engineOn() { throw new IllegalStateException("not supported"); } ... } public class Car implements Vehicle { ... public boolean pedalsMove() { throw new IllegalStateException("not supported"); } } --- usage --- private static void printIsMoving(Vehicle v) { if (v instanceof Car) { System.out.println(v.engineOn());} if(v instanceof Bike) {System.out.println(v.pedalsMove());} } Example 1.8 – Various implementations of inherited method abstraction 18 Getting into Software Design Patterns Some of you with a keen eye will already notice that such a software design direction negatively involves software flexibility through unnecessary actions that need to be considered (such as exceptions). The remedy is based on compliance with the ISP in a very transparent way. Consider two additional interfaces, HasEngine and HasPedals, with their respective functions (see Example 1.9). This step forces the printIsMoving method to overload. The entire code becomes transparent to the client and does not require any special treatment to ensure code stability, with exceptions as an example (as seen in Example 1.8): public interface Vehicle { void setMove(boolean moving); } public interface HasEngine { boolean engineOn(); } public interface HasPedals { boolean pedalsMove(); } public class Bike implements HasPedals, Vehicle {...} public class Car implements HasEngine, Vehicle {...} --- usage --- private static void printIsMoving(Vehicle v){ // no access to internal state } private static void printIsMoving(Car c) { System.out.println(c.engineOn()); } private static void printIsMoving(Bike b) { System.out.println(b.pedalsMove()); } Example 1.9 – The functionality split into smaller units (interfaces) based on the purpose Two interfaces, HasEngine and HasPedals, are introduced, which enforce method code overload and transparency. The dependency inversion principle (DIP) Every programmer, or rather software designer, will face the challenge of hierarchical class composition throughout their careers. The following DIP is a remarkably simple guide on how to approach it. Significance of design patterns 19 The principle suggests that a low-level class should not know about high-level classes. In the opposite direction, this means that the high-level classes, the classes that are above, should have no information about the basic classes at lower levels (see Example 1.10, with the SportCar class): public interface Vehicle {} public class Car implements Vehicle{} public class SportCar extends Car {} public class Truck implements Vehicle {} public class Bus implements Vehicle {} public class Garage { private List parkingSpots = new ArrayList(); public void park(Vehicle vehicle){ parkingSpots.add(vehicle); } } Example 1.10 – The garage implementation depends on vehicle abstraction, not concrete classes in a hierarchy It also means that the implementation of a particular functionality should not depend on specific classes, but rather on their abstractions (see Example 1.10, with the Garage class). Significance of design patterns The previous sections introduced two complementary approaches to software design – APIE and SOLID concepts. It has begun to crystallize that having code in a transparent form can be beneficial for a variety of reasons, because every programmer often, if not always, faces the challenge of designing a piece of code that extends or modifies existing ones. One wise man once said, “The way to Hell is the path of continual technical debt ignorance....” Anything that slows down or prevents the development of applications can be considered a technical debt. Translated into a programming language, this would mean that even a small part matters, if not now, then later. It also follows that code readability and purpose are crucial to application logic, as it is possible to verify various hypotheses (for example, application operation). The inability to perform business-oriented application testing can be considered the first sign of incorrect development trends. It may appear to require the use of different mock-up techniques during verification. This approach can easily turn into providing false-positive results. This can usually be caused by the clutter of the code structure, which forces programmers to use mocks. 20 Getting into Software Design Patterns Although the SOLID and APIE concepts suggest several principles, they still do not guarantee that the project code base will not start to rot. Adherence to these principles makes it difficult, but there is still room because not all concepts provide the required framework for dealing with rot. There may be long stories of how software can rot over time, but one fact that remains is that there is a cure for avoiding it or letting it go. The cure is covered by an idea called design patterns. The idea of a design pattern not only covers the readability of the code base and its purpose but also advances the ability to verify required business hypotheses. What are the ideas behind defining it to get more clarity? The design pattern idea can be described as a set of reusable coding approaches that solve the most common problems encountered during application development. These approaches are in line with the previously mentioned APIE or SOLID concepts and have an incredibly positive impact on bringing transparency, readability, and testability to the development path. Simply put, the idea of design patterns provides a framework for accessing common challenges in software design. Reviewing what challenges design patterns solve Take a deep breath and think about the motivation for writing the program. The program is written in a programming language, in our case, Java, and is a human-readable form to address a specific challenge. Let’s look at it from a different perspective. We can state that writing a program is considered a goal. The goal has its reason defined by known needs or requirements in most cases. Expectations and limitations are defined. When the goal is known, each action is chosen with the aim of achieving it. The goal is evaluated, organized, and placed in the context of the destination, where the destination means a work program addressing the required challenge. Imagine all the difficulties mentioned in the previous sections. Day after day, a new solution is posed, instead of a transparent solution. Every day, another local success keeps the project afloat, despite everything looking good on the surface. Currently, most teams follow the SCRUM framework. Imagine a situation where the team follows the SCRUM framework (see Reference 4) and application development begins to deviate from the goal. Daily standup meetings run smoothly from time to time: it is mentioned that a fundamental error has been found. A few days later, the bug is successfully fixed with great applause. Interestingly, the frequency of such notifications is growing – more corrections, more applause. But does this really mean that the project is moving towards its goal? Does this mean that the application works? Let’s look at the answer. There is a darker side – the backlog is growing with features and technical debt. Technical debt is not necessarily a terrible thing. Technical debt can stimulate the project and can be especially useful in the concept validation phase. The problem with technical debt occurs when it is not recognized, ignored, and poorly evaluated – even worse when technical debt starts being labeled as new features. Reviewing what challenges design patterns solve 21 Although the product backlog should be one entity, it begins to consist of two different and unfortunately incompatible parts – the business and the sprint backlog (mostly technical debt). Of course, the team is working on a sprint backlog that comes from planning meetings, but with increasing technical debt, there is less and less room for the relevant business functions of the product. The trends observed in this way can result in extremely tricky situations during each new sprint planning session, where the development resources should be allocated. Let’s stop for a moment and recall this situation where the team cannot move the product forward due to technical debt. The values of the SCRUM methodology can be simplified to courage, concentration, determination, respect, and openness. These values are not specific to the SCRUM framework. Because the team’s motivation is to deliver the product, they all sound very logical and fair. We will now refresh our memory of the state the team has achieved. A state where it cannot move the project forward and struggles with the definition and proper consolidation of technical departments. This means that the team is doing its job, but may deviate from achieving its ultimate goal. Every discussion is extremely difficult because it is difficult to solve and describe the problem correctly for many different reasons. It may seem that developers may lose their language of communication and begin to misunderstand each other. We can see that the entropy of the software has increased because the coherence is not maintained. The project is beginning to rot and convergence to the inevitable wasted development time increases. Let us take another deep breath and think together about how to prevent such a situation. It must be possible to identify these tendencies. Usually, each team has some commonality: the team is not always homogeneous in terms of knowledge, but this should not prevent us from identifying the degradation of the learning curve. The project learning curve can help us identify a rotting project. Instead of gradual improvements towards the goal, the team experiences local successes full of technical repairs and solutions. Such successes do not even correspond to the values of SCRUM and gradual improvement seems unlikely. The solution may not be considered an improvement because it is specific to a particular movement and may violate the specifications of the technology used. During the solution period, the team may not acquire any useful knowledge applicable to the future. This can soon be considered a missing business opportunity due to the inability to supply business elements or only parts of them. In addition to the degradation of the learning curve, other symptoms can be identified. This can be described as an inability to test a business function. Project code is proving sticky, dependencies are out of control, which can also harm code readability, testability, and, of course, programmer discipline. The daily goal of the software designer can be reduced to closing a ticket. To avoid getting to this state, this book will provide some guidelines for solving the most common problems in the following chapters by introducing and questioning different types of design patterns. The design patterns are in line with the aforementioned basic pillars of OOP and APIE and promote the principles of SOLID. 22 Getting into Software Design Patterns What’s more, design patterns can highlight any misunderstood directions and enforce the don’t repeat yourself (DRY) principle. As a result, there is much less duplication, code testability, and more fun on the project. That brings us to the end of this chapter. Summary Before we embark on the journey of researching design patterns, let us quickly summarize. This chapter has expanded or improved our understanding of various areas. Each of these areas affects program code from different perspectives: Code transparency and readability The ability to solve complex challenges Following SOLID and OOP principles Code testability (it’s possible to verify the purpose of the code) Easy to extend and modify Supporting continual refactoring Code is self-explanatory The program code is written – well done. The next chapter will take us through a survey of the implementation platform – in our case, the Java platform. We will learn in more detail how and what it means to run a program. Questions 1. What interprets the Java code to the platform and how? 2. What does the acronym APIE represent? 3. What types of polymorphism does the Java language allow? 4. What principle helps software designers to produce maintainable code? 5. What does the OCP mean? 6. What should be considered about design patterns? Further reading 23 Further reading The Garbage Collection Handbook: The Art of Automatic Memory Management, Anthony Hosking, J. Eliot B. Moss, and Richard Jones, CRC Press, ISBN-13: 978-1420082791, ISBN-10: 9781420082791, 1996. Design Principles and Design Patterns, Robert C. Martin, Object Mentor, 2000. Keynote address - data abstraction and hierarchy, Barbara Liskov, https://dl.acm.org/ doi/10.1145/62139.62141, 1988. The SCRUM framework, https://www.scrum.org/, 2022. 2 Discovering the Java Platform for Design Patterns Many years ago, motivated by the lack of a suitable Application Programming Interface (API) design, something extraordinary began to happen. In the early days of using the World Wide Web (WWW), the direction of application development was a bit shrouded in fog. In one direction, there was a strong need in the industry to process a large number of database transactions or develop specific proprietary hardware and software. On the other hand, it was not clear what kind of applications might be needed to move the demand forward and how such an application should be maintained. In this chapter, we will prepare the ground for understanding the value of design patterns from a memory utilization perspective. We will do so by covering the following topics: The rise of Java and brief historical facts How the Java platform works under the hood Exploring Java memory area allocation and management How allocated heap is maintained with garbage collection Running the first program on the platform The threading nature of the Java platform Examining the core Java APIs and their values for software design Exploring the importance of the Java Platform Module System Discovering new helpful platform enhancements Introducing Java concurrency 26 Discovering the Java Platform for Design Patterns By the end of the chapter, you will have a good understanding of memory allocation on the Java platform, platform guarantees, core APIs, and more. Together with the content of the previous chapter, these topics will form a well-prepared foundation so that you can start with design patterns with full awareness of their benefits. Technical requirements The code files for this chapter are available on GitHub at https://github.com/ PacktPublishing/Practical-Design-Patterns-for-Java-Developers/tree/ main/Chapter02. Knocking on Java’s door In the early 1990s, a small team at Sun Microsystems was formed in order to discover new horizons. The team started with the consideration of extending the C++ features available in those days. One of the goals was to introduce a new generation of software for a small smart device. The introduction of software reusability was a part of this. Small smart devices such as set-top boxes did not have much memory and had to use their resources wisely. The memory, among other things, such as its complexity, error-prone programs, and probably James Gosling’s language extension attempt, later led to the rejection of the C++ idea. Instead of struggling with C++, a new language called Oak was created in lieu. Due to the trademark issue, the newly created language Oak was renamed Java. The first public Java version 1.0a.2, together with HotJava Browser, was announced at the SunWorld conference in 1995 by John Gage, the director of science at Sun Microsystems. He was involved in re-directing the Java language from being a language for small hardware devices to being a platform for WWW applications. In these early days, Java was used as part of a website using a technology known as an applet. Java applets were small sandboxes, defined by the frame with limited access and the capability to execute Java bytecode on the local Java Virtual Machine (JVM). Applets resided on a web browser or as a standalone application; they were a very powerful tool that supported one of the basic Java principles, Write Once, Run Anywhere (WORA). However, due to many issues (such as security and stability), the applet technology was marked for removal (Java SE 17). Exploring the model and functionality of the Java platform 27 The Java platform consists of three main parts (Figure 2.1): Figure 2.1 – Java Development Kit architecture These parts are the following: A JVM The Java SE (Standard Edition) Runtime Environment (JRE) The Java SE Development Kit (JDK) Let us start an exciting journey through the platform itself and each part. Exploring the model and functionality of the Java platform History has shown us that the intended direction can evolve or change: Java is a nice example and is no exception. From its original purpose, it has moved from a platform for smart devices to a platform for entire web solutions, but its development did not stop there. Over the years, Java has become one of the most widely used languages for application development. This can be taken as a side effect of basic hardware independence. It dramatically developed an available set of tools and received a very positive response from a vibrant community. Let us review each part of the platform (from Figure 2.1) individually as it will boost our understanding of writing code. 28 Discovering the Java Platform for Design Patterns The JDK The JDK is a software development environment that provides the tools and libraries needed to develop and analyze Java applications. The JDK provides a collection of basic libraries, functions, and programs needed to compile written code into bytecode. The JDK contains the JRE required to run the application. The JDK also provides some very useful tools, such as the following examples: jlink: This helps generate a custom JRE jshell: This is a handy Read-Evaluate-Print-Loop (REPL) tool to try the Java language jcmd: This is a utility to send a diagnostic command to the active JVM javac: This is the Java compiler, which reads an input file with the.java suffix and produces a Java class file with the.class suffix java: This executes a JRE Others: Located in the JDK bin directory The code is written (Example 2.1) and stored in a.java file and compiled using the javac command: public class Program { public static void main(String... args){ System.out.println("Hello Program!"); } } Example 2.1 – Simple Java program as an executable class that can also be run directly without a compilation step since Java SE 11 (Reference 26) Next, it is possible to create and compile a class with bytecode inside (Example 2.2). Run the file using the java command to run the JRE:... public static void main(java.lang.String...); descriptor: ([Ljava/lang/String;)V flags: (0x0089) ACC_PUBLIC, ACC_STATIC, ACC_VARARGS Code: stack=2, locals=1, args_size=1 0: getstatic #7 // Field java/lang/ System.out:Ljava/io/PrintStream; Exploring the model and functionality of the Java platform 29 3: ldc #13 // String Hello Program!... Example 2.2 – Bytecode example from a compiled program displayed by the Java program The JRE The JRE is part of the JDK, or it can be distributed as a standalone program for the target operating system. To run a file with a.class extension or a Java Archive (JAR) file, the target system is required to contain the appropriate version of the JRE. Unlike the JDK, the JRE only contains a minimal collection of components needed to run the program, such as the following: Core libraries and property files: for example, rt.jar and charset.jar Java extension files: Additional libraries that may reside in the lib folder Security-related files: Certificates, policies, and so on Font files Operating system-specific tools The JRE includes a JVM and precisely two types of compilers: Client Compiler: Fast loading without optimization. It is designed to run the instructions to obtain a result very quickly. Commonly used for standalone programs. Server Compiler: Loaded code goes through additional checks to ensure code stability. There is also an effort to produce highly optimized machine code to deliver better performance. It supports better statistics in order to run machine code optimization executed by the Just-in- Time (JIT) compiler (Figure 2.2). The JVM Both the JDK and the JRE contain the JVM (Reference 6). The JVM is platform-dependent. This means that every system platform requires the use of a dedicated version. Fine, but what does the JVM really do, and how? Although there are multiple versions of the JVM, even from multiple vendors, the JVM itself is defined by a specification that must be followed. The reference implementation is represented by OpenJDK. In fact, OpenJDK is a collection of several smaller open source projects that may even have different development dynamics, but the OpenJDK release contains planned versions of each. 30 Discovering the Java Platform for Design Patterns Figure 2.2 – Key parts of the JVM The OpenJDK JVM implementation (Figure 2.2) includes a JIT compiler called HotSpot (Reference 7). HotSpot is part of the JVM and its responsibility is runtime compilation. In other words, the JIT compiler translates or compiles the provided bytecode into a native system instruction at runtime. This process is sometimes called dynamic translation. Due to these JVM dynamic translation capabilities, Java applications are sometimes referred to as system platform-independent and the WORA acronym is used. This statement needs to be abstracted slightly because a JVM system implementation is required to translate the bytecode into a native instruction. Exploring the model and functionality of the Java platform 31 In addition to the JVM JIT compiler, it includes a garbage collector with various algorithms, a class loader, a Java memory implementation model, and a Java Native Interface (JNI) with libraries (as shown in Figure 2.2). Every JVM provider must follow the specifications. This guarantees that the bytecode will not only be created accordingly but also executed and correctly converted into machine instructions. This means that different vendors may provide different JVM implementations with slightly different metrics or optimizations, such as garbage collector dynamics. These vendors include IBM, Azul, Oracle, and so on. The diversity of vendors can be considered one of the main moving factors for the Java platform's evolution. New features are extended or modified through the JDK Enhancement Proposal (JEP), where each vendor can contribute or get a very detailed overview. To summarize, the JVM’s responsibilities to remember are as follows: Loading linking Initiating classes and interfaces Program instruction execution The JVM defines several different areas used by each program (Example 2.2). Let’s look at each of them one by one, area by area (Figure 2.2). This can boost our understanding of the value of design patterns and their approaches, such as the Builder or Singleton pattern. Figure 2.3 – Simplified schema of program compilation and execution It all starts with written text, representing a program stored in a.java file. The file will be compiled (Figures 2.3) and run (Figure 2.4) and threads are started. Startup starts the system process in which the JRE is running and the JVM is running as part of the JRE. 32 Discovering the Java Platform for Design Patterns Figure 2.4 – Threads that started behind the scene event for a Program. java execution example (Java Flight Recorder) With a general idea of the flow, let’s start by loading classes into memory. The class-loader loader area The class loader subsystem is located in Random-Access Memory (RAM) and is responsible for loading classes into memory. The load step consists of the sub-line steps and the first run of the class at runtime. Linking is the process of preparing a class or interface for a runtime environment, which may include internal dependencies, for example. The platform provides internal functions or customized ones; to manage all these capabilities, the platform provides dedicated class loaders: Bootstrap class loader: Responsible for loading the default platform classes. It is provided by JVM and loads classes from the BOOTPATH (property). Extension class loader: Loads the additional libraries from the lib/ext directory, which is a part of the JRE installation. System class loader: The default application class loader that refers to the main method and runs the classes from the served class or module path. User-defined class loaders: These are instances of ClassLoader and may be used to define custom classes dynamically loading processes to the JVM. It is possible to use a user-defined class destination. Classes can reside on the network, be encrypted inside files, or be downloaded across the network and generated on the fly. Exploring the model and functionality of the Java platform 33 Class loaders work in sequence. The sequence is represented by a hierarchy. This means that every child must refer to its parents. This automatically defines the search order of the binary classes. When a class is present in RAM, the Java platform takes action to make the class available to the runtime environment. The Java platform runs several processes behind the scenes to move relevant class data to other areas, such as the stack, heap, and so on. Let’s look at the stack area next. The stack area The stack area (Figure 2.2) is reserved for each thread at runtime. This is a small area for storing method references. When a thread executes a method, one entry for that method is created and moved to the top of the stack. This kind of item is called a stack frame, which has a reference to a field of local variables, a stack of operands, and a constant pool to identify the appropriate method. The stack frame is removed when the method is executed normally – that is, without causing any exceptions. This means that local primitive variables such as boolean, byte, short, char, int, long, float, and double are also stored here, so they are not visible to the second thread. Each thread can pass a copy, but this does not share the origin. The heap area The heap is the allocated memory where all instances of the class and array are located. The heap is allocated at startup and is shared among all JVM-initiated threads. Allocated memory is automatically recovered by the automated management system process, also known as Garbage Collection (GC). A local variable can contain a reference to objects. The referenced object is located in a heap. The method area The method area is shared across all JVM-initiated threads. The area is allocated during the JVM startup time. It contains runtime data for each class, such as a constant pool, field and method data, the code for constructors, and methods. Probably the most unfamiliar term mentioned is the constant pool. The constant pool is created during the process of loading the class into the method area. It contains the initial values of string and primitive constants, the names of the reference classes and other data needed to properly execute the loaded class, the constants known at compile time, and field references that must be resolved at runtime. Program counter The Program Counter (PC) register is another important reserved area in memory. It contains a list of created program counters. A PC record is created at the beginning of each thread and contains the address of the currently executed instruction by a specific thread. The address points back to the method area. The only exception is the native method, which leaves the address undefined. 34 Discovering the Java Platform for Design Patterns The native method stack A native method stack record is initiated for each individual thread. Its function is to provide access to native methods through the JNI. The JNI operates with the underlying system resources. Improper usage may turn into two exceptional states: The first exception appears when a thread requires more stack space. In this case, a StackOverflowError error is thrown and the program crashes, executed with a state higher than 1. The second case represents an attempt to add more entries to the stack. The program results in an OutOfMemoryError error. It is caused by an attempt to dynamically expand already fully allocated memory space. The memory is insufficient and it is not possible to allocate a new stack for the newly intended thread. We have examined all the areas required to load and execute a program and we will get acquainted with the areas where the data is located and how they are interconnected. It is slowly becoming clear that in order to achieve stability and maintainability of the program at runtime, it is necessary to design the software in a way that reflects the potential limitations, as the reserved areas correspond to the individual areas. Let’s take a closer look at how the Java platform provides available memory space for each newly created object. Reviewing GC and the Java memory model We mentioned the JIT compiler as part of the JVM earlier (Figure 2.2). Just to refresh on the JIT compiler, it is responsible for translating the bytecode into system-specific native instructions. These instructions deal with the basic memory and I/O resources available to the program. To properly organize these instructions, the Java platform requires a set of rules that guarantee the program, called bytecode, which must be translated by the JIT compiler at runtime to the same end. Because the Java platform does not use physical memory directly, but rather virtual and cached views, it is very important that the memory management is transparent. The model must provide the required guarantees and is known as the Java Memory Model (JMM). The JMM The JMM describes how threads interact with each other through access to allocated memory – the heap (Figure 2.2). The execution of a single-threaded program may seem obvious because the instructions are processed in a certain order without external influence and the thread is in isolation. In the case of a single thread (see the main method in Example 2.2 and the main thread in Figure 2.4), the run areas are modified each time the instruction is executed; there is no surprise. The situation changes when the program starts multiple threads. The JMM enforces its guarantees of reliable Java program execution. The JMM defines a set of rules for possible instruction order changes and execution restrictions Reviewing GC and the Java memory model 35 caused by sharing objects in memory between different threads. The fact that the JMM strictly follows these rules forces JIT optimization without fear of code instability (maintaining a consistent state). The rules can easily be reformulated and each action can be changed as long as the execution of the thread does not violate the program order. Basically, this means that the program remains in a consistent state. Object locks or releases are governed by the order of the program and each thread shares a corresponding memory view of the modified data. The memory view represents the portion of allocated physical memory represented by the heap because each object created is located inside the heap. One of the important guarantees of the JMM is known as happens-before. It states that one action always happens before another in order to maintain the order of the program. To better understand this rule, it is necessary to describe how system memory works and briefly introduce the general types of memory and how the CPU fits into the process of reading values and executing machine instructions. Let us start with the CPU. Each CPU contains its own instruction register. The machine code compiled by the JIT compiler has a reference to an available set of instructions. The CPU contains an internal cache used to store a copy of data from the main RAM. The CPU communicates with the reserved RAM. One CPU can run multiple platform threads (depending on the type of CPU) at the same time. The result of this embodiment modifies the state of the RAM in the thread stack or heap. The dedicated RAM for the running Java application is then copied to the CPU cache and used by the CPU registry (Figure 2.5). Figure 2.5 – CPU and memory interaction Those who are attentive may have already noticed that due to memory differentiation, the program can face unpredictable difficulties caused by looking at the program’s memory. When multiple threads try to update or read specific values of variables without careful handling, this can result in one of the following problems: A racing condition: This occurs when two threads attempt to access the same value in an unsynchronized manner. Value update visibility: A variable update that is shared between multiple threads has not been propagated to the main memory, so other threads get the old value. 36 Discovering the Java Platform for Design Patterns To address these challenges, let’s analyze a real access to variables. What is already known is that each value is located within the allocated RAM heap. It seems obvious that updating the status of each variable may cause some penalties, as each instruction has to take a whole journey (Figure 2.5). In most cases, this is also not necessary. A good example is the implementation of an isolated method (Example 2.2). However, there are cases where the actual value of a variable is required from memory, for which the Java platform has introduced the volatile keyword. Using the volatile keyword before a variable gives the variable a guarantee that when another thread requests a value, it checks its current value in the main memory. This means that using the volatile keyword provides a guarantee of happens-before and each thread sees its true value. It is fair to note that because using volatile provides a certain level of memory synchronization, it should be used wisely. Its use is associated with performance limitations caused by main memory access. Another approach to sharing variable values across multiple threads is to use the synchronized keyword. Its use gives the method or variable a guarantee that each participant, the thread, will be informed about the approaches. Obviously, the main disadvantage of using synchronized is that all threads will be informed about access to the method or variables, which in turn, will cause a decrease in performance due to memory synchronization. As with volatile, synchronized guarantees happens-before. The JMM is bright and fresh; we stated that each new object is located in the heap (Figure 2.2). We are familiar with the big picture of the JRE architecture, and we know that most Java programs seem to be multi-threaded – there is a set of rules that the Java platform follows so that the process forces the correct order of programs to achieve consistency. GC and automatic memory management Although the Java platform may give the impression that the underlying memory is unlimited, this is not true and we will examine it next. So far, we have looked at how variable visibility works across multiple threads and how values are referenced in physical memory. The JMM is just one part of the whole story – let’s continue the investigation. We already know that the Java platform uses an automatic memory management process to maintain the allocated memory for the heap. Part of the process is a program that runs in the background of the daemon thread. It is called the garbage collector (Reference 5) and runs silently behind the scenes, reclaiming and compacting unused memory. This is one of the advantages of the dynamic allocation of objects in the heap. Another perhaps less obvious advantage is the ability to work with recursive data structures, such as lists or maps. GC was invented around 1959 by John McCarthy. The goal was to simplify manual memory management in Lisp. Since then, GC has undergone massive development and various GC techniques have been invented (Reference 1). Even after the development of various GC approaches, the security rule remains the most important. The GC should never regain a repository of live objects that contain active references. Reviewing GC and the Java memory mo