Are you prepared for questions like 'Can you explain why Java is not 100% Object-oriented?' and similar? We've collected 40 interview questions for you to prepare for your next Java interview.
Did you know? We have over 3,000 mentors available right now!
Java is primarily an object-oriented programming language, but it's not considered 100% object-oriented because it supports primitive data types such as int, float, char, boolean, etc., which are not objects. In a strictly object-oriented language, all data types, without exception, would be based on objects.
However, Java has chosen to include these eight primitive data types for efficiency. They consume less memory and their values can be retrieved more quickly compared to objects. For instance, an 'int' in Java typically uses 4 bytes of memory, while the equivalent Integer object uses a lot more.
The mix of object-oriented principles with the inclusion of primitive data types aims to strike a balanced approach, leveraging both the advantages of object-oriented concepts and the efficiency of simple, non-object data handling.
A process and a thread are two fundamental units of execution in computing, but they're used in different ways.
A process is essentially a program in execution. It holds the set of instructions loaded into memory and ready to run, along with its own memory space, system resources, and a handle to the operating system. Every process gets its own separate memory space from the operating system.
A thread, on the other hand, is a subset of a process. A process can spawn one or more threads, and all threads belonging to a process share the process's memory space and resources. This makes inter-thread communication much easier than inter-process communication.
For instance, in a web browser (which is a process), each webpage you open might be handled by a separate thread. They all share the browser's resources (like memory space), but they perform their tasks (like loading the webpages) concurrently, which makes the browser more efficient.
So, in short, processes are isolated execution contexts that contain one or more threads, while threads are individual execution paths that can run concurrently within a process, sharing its resources.
An abstract class in Java is a superclass that cannot be instantiated and is used to provide a base for subclasses to extend. It can have both abstract methods (those without a body) and non-abstract methods (regular methods with an implementation). Abstract classes are great when you want to provide common, implemented functionality for all subclasses, but also want to force each subclass to implement some methods in their own way.
On the other hand, an interface is like a completely abstract class - it can only declare methods but not implement them. It provides a contract for classes, where the classes agree to implement the methods defined in the interface. However, starting from Java 8, interfaces can include default methods with implementations and static methods, which somewhat blurs the distinction between them and abstract classes.
Here's a crucial difference, a class can extend only one abstract class, but it can implement multiple interfaces. This makes interfaces a good workaround for Java's lack of multiple inheritance, where you want a class to inherit the behavior of more than one parent.
Multithreading in Java is a feature that allows concurrent execution of two or more parts of a program for maximum utilization of CPU. Threads in Java are created and controlled by the java.lang.Thread class. Each thread starts in the new thread context, splits off from the main memory stack, and works independently. When one thread's execution causes an exception, it doesn't affect the other threads and they continue their execution.
When a Java program starts, one thread begins running immediately. This is usually called the 'main' thread, because it's the one that is executed when your program begins. However, you can spawn additional threads from the main thread, and these can all run concurrently, either sharing the same resources or having resources of their own. The JVM schedules these threads to run, switching between them to give the illusion of simultaneous execution.
The real power of multithreading is apparent in multi-core or multi-processor systems where threads can truly run in parallel, greatly improving the performance of your Java applications, by allowing tasks such as reading files, calculating results, or responding to network requests to happen independently and in parallel.
The 'static' keyword in Java has a special role. When a member (variable, method, or nested class) is declared static, it means it belongs to the class itself rather than any instance of that class.
For variables, declaring them as static means there's only one copy of the variable, no matter how many instances (objects) of the class you create. It's kind of like a shared variable.
For methods, making them static means you can call them without creating an instance of the class. This is often used for utility or helper methods, where creating an object would be unnecessary overhead. Main methods in Java are always marked as static, so that JVM can call them without creating an instance.
Static nested classes are just like any other outer class and can be accessed without having an instance of the outer class.
In essence, 'static' keyword helps in memory management as static members are shared across all instances of a class, and also allows for methods to be called without needing an instance of the class.
Overloading and overriding in Java are two key concepts related to methods, and they serve distinct purposes.
Overloading happens when two or more methods in the same class have the same name but different parameters. This is used to provide different ways to use a single method with different input data types, counts or orders. For example, a method can be overloaded to accept either two integers or two strings as arguments.
Overriding, on the other hand, occurs when a subclass provides its own implementation for a method that is already found in its parent class. This way, the version of the method called will be determined by the object's runtime type. Overriding is one of the core behaviors of polymorphism in object-oriented programming.
So essentially, overloading is about using the same method name with different parameters, whereas overriding is about changing the behavior of a method inherited from a superclass in a subclass.
The 'final' keyword in Java serves a few different purposes depending on where it's used. When 'final' is applied to a variable, it means that variable's value cannot be changed once it's assigned; it essentially becomes a constant. For instance, you might use this when defining configuration settings or other types of data that must stay the same throughout the execution of your code.
When you apply 'final' to a class, it means the class cannot be subclassed. This can be particularly useful when you want to ensure the integrity and security of a class and prevent any changes to it.
Lastly, using 'final' for a method prevents that method from being overridden by subclasses. This is useful for preserving the functionality of a method no matter the subclass it's used in. So, in short, 'final' is all about lockdown; it's there to assert control on variable modification, class inheritance, and method overriding.
Inheritance in Java, and in object-oriented programming in general, is a mechanism where you can create a new class that's based on an existing class. The new class, called a subclass, inherits fields and methods from the existing class, known as the superclass.
Inheritance is typically used when you have classes that share common characteristics. For example, if you're designing classes to represent different types of vehicles like Car, Truck, and Bike, you might create a superclass Vehicle with fields for speed and weight, and methods like start and stop. Then Car, Truck, and Bike classes can inherit from Vehicle, so you don't need to duplicate the speed, weight, start and stop in each of these classes.
Inheritance is also key for enabling polymorphism, where a superclass reference can point to any of its subclass objects. This becomes extremely useful when designing large functional units using the "programming to an interface" principle.
When used judiciously, inheritance can make your code more readable and maintainable, by reducing duplication and promoting reusability. But it's good to be cautious as overuse can lead to complex and tightly-coupled designs. It's often suggested to use composition (combining simple objects to create more complex ones) over inheritance where possible.
A Marker interface in Java is an interface with no fields or methods. Put simply, it's an empty interface. The purpose of a marker interface is to "mark" a class that implements the interface as having a certain property. These are used to signal to the Java compiler that the objects of the classes implementing the interfaces with no defined methods need to be treated differently.
The most common examples of marker interfaces from the Java Development Kit (JDK) are the Serializable and Cloneable interfaces. They are used to indicate that a class can be serialized into streams or could be cloned, respectively.
Java annotations, which were introduced in Java 1.5, provide a more powerful and flexible way of marking classes for certain behaviors and as such, the use of marker interfaces has declined. However, marker interfaces are still in use because they have one advantage over annotations. By using a marker interface, you can make use of the strong typing that the Java language offers for compile-time safety and clarity.
The Java Memory Model deals with how the computer's memory works in the context of concurrent programming. It covers how changes to memory by one thread are made visible to other threads, and also guarantees atomicity for certain operations on variables. The Java Memory Model defines behaviors and interactions of threads, memory and data, essentially serving as a specification that aids developers in writing multithreaded programs.
Java memory is divided into two main regions: heap and stack. The heap is where objects live. Every time an object is created, it's stored in the heap and any variables that refer to that object are linked to that memory space. The Java Garbage Collector operates in this region, deallocating memory once objects are no longer in use.
The stack memory, in contrast, is used for static memory allocation and contains primitive values that are specific to a thread and method-execution specific information such as local variables and partial results. Once a method invocation is completed, the stack frame gets destroyed and thus makes stack memory an efficient choice for storage that has a short lifespan.
Understanding the Java Memory Model is crucial for writing safe and efficient concurrent code, as it helps developers to avoid common multithreading pitfalls like race conditions, improve thread interaction and synchronization, and manage memory more effectively.
The 'volatile' keyword in Java plays a crucial role in the context of multithreading. It is used to indicate that a variable's value can be modified by different threads.
The primary purpose of 'volatile' is to ensure that updates to a variable are propagated predictably to other threads. When multiple threads using the same variable, there's a chance of cached variable value in a thread's own stack, that can lead to 'dirty read' due to differences between the cached value and the actual value in main memory. The 'volatile' keyword ensures that the value of the variable is always read from main memory, not from the thread's local cache, and that when the value is updated, it's written directly to main memory, ensuring that all threads see the most current value.
In addition, the 'volatile' keyword also provides a 'happens-before' relationship, which ensures that memory writes by one specific statement (say a write to a volatile variable) are made visible to all subsequent reads of that same variable by any thread.
However, while 'volatile' does help in synchronizing data access, it's important to note that it isn’t a complete substitute for a 'synchronized' block or a Lock as it doesn't prevent race conditions from methods where multiple variables are involved or where multiple operations need to be atomic as a whole.
The Java Reflection API provides the ability to inspect and manipulate classes, interfaces, constructors, methods, and fields at runtime, without knowing the names of the classes, methods etc. at compile time. It's essentially for introspection of the code.
For example, imagine you have an object and you want to call a method on it, but you only know the method's name as a string. You can use the Reflection API to get a Method object, and then invoke the method on the object. You can do similar things for constructors, fields, and even arrays.
It's also used to retrieve class details, such as the names of its methods or fields, its parent class, implemented interfaces, and so on. Another use might be to create an instance of a class dynamically, once again without knowing the class name at compile time.
Though Reflection is powerful, it comes with a few caveats. It can break encapsulation by allowing access to private fields and methods, which can lead to security risks and maintenance issues. It also tends to be slower than non-reflective code. So, it's recommended to use it sparingly, only in situations where its benefits outweigh the drawbacks.
Design patterns in Java, or in any programming language for that matter, are proven solutions to common software design problems. They provide a standard terminology and a specific approach to specific issues that can come up during software development.
For example, consider the Singleton pattern. This design pattern enforces the concept of having only one instance, or object, from a class at any given time. This is useful in situations where having more than one instance could cause problems, such as controlling access to a shared resource.
Another example is the Observer pattern, used when you need to have a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically. This is commonly used in event handling systems.
Using design patterns can make your software easier to understand, easier to modify, and more robust. However, it's also important to remember that design patterns aren't a one-size-fits-all solution. They need to be chosen based on the specific needs and context of your software project. And unnecessary use of design patterns can overcomplicate the design and make the code more difficult to understand and maintain.
Deadlock in Java is a situation where two or more threads are blocked forever, each waiting for the other to release a lock. It usually happens in the context of multiple threads holding multiple locks, where each thread needs to acquire the others' locks to complete execution but won't release its own lock for the others to proceed.
Identifying and preventing deadlocks involves careful design as Java runtime environment doesn't automatically handle deadlocks.
One strategy for avoiding deadlock situations is to impose some ordering on the acquisition of locks. For example, you can assign an order to your locks and establish a rule that all locks must be acquired in that order. If a thread wants to acquire a lock, it drops all its currently held locks, and re-acquires them in the prescribed order, including the new lock. This can prevent circular dependency of locks, essentially removing the possibility of a deadlock.
Other strategies include using Java's built-in lock timeout features to have threads give up after waiting for a lock for a certain amount of time. This brings awareness to the issue and allows you to design a proper handling mechanism.
Proper use of the 'synchronized' blocks as opposed to entire methods, and minimal use of nested synchronization, are some of the practices to avoid deadlocks. However, each of these strategies need careful considerations as they come with their own tradeoffs. Therefore, diligent design, rigorous code reviews and thorough testing play an important role in dealing with potential deadlock situations.
In Java, concurrent access to shared resources can be managed using several techniques, with the most common being the use of synchronized blocks or methods.
The 'synchronized' keyword in Java ensures that only a single thread can access the method or block at a given time, enforcing mutual exclusion. When a thread enters a synchronized method or block, it acquires a lock on the object the method or block is synchronized on. Other threads that attempt to enter this synchronized context have to wait until the first thread releases the lock, i.e., finishes the method or block.
Java also has a higher-level framework known as the java.util.concurrent package that provides more flexible and powerful mechanisms like semaphores, locks, barriers, and atomic variables to manage concurrency. For example, a Lock interface can offer more extensive operations like attempting to acquire a lock in a non-blocking way or with a timeout.
It's essential to handle concurrent access properly to avoid issues like race conditions, where unsynchronized updates to shared resources can result in inconsistent and unpredictable outcomes. However, it's also crucial to avoid over-synchronization, which can lead to problems like thread contention and deadlocks, negatively impacting the performance of your application.
Java 8's Stream API introduced a new abstraction of data in the form of sequences of elements, including methods to perform aggregate operations on these elements. It offers a more declarative approach to perform complex data processing queries.
Stream operations are either intermediate or terminal. Intermediate operations, such as 'filter' or 'map', return a new stream and are always lazy - executing an intermediate operation such as filter does not actually perform any filtering, but instead creates a new stream that, when traversed, contains the elements of the initial stream that match the given predicate. Terminal operations, such as 'collect', 'count', or 'forEach', produce a result or a side-effect. Streams are not reusable and once a terminal operation is called, the stream is consumed.
A common use case in my applications included transforming a List of objects into another List of objects based on some properties of the original objects. For example, converting a List of Users, each with a name and email, into a List of Strings representing the email addresses. This could be done like this:
List<String> emails = users.stream().map(User::getEmail).collect(Collectors.toList());
This approach is simpler, cleaner and easier to understand than traditional for-loops. Other use-cases could include filtering and grouping data, finding sum or average or any statistical measures.
Overall, the Stream API can result in greatly simplified and expressive code, especially when dealing with large sets of data. However, developers should pay attention to how they use it, as the operations are composed lazily and can lead to inefficient calculations if not used correctly.
Java handles memory allocation (and deallocation) automatically so that developers don't have to write code to allocate or deallocate memory like you would in languages such as C or C++.
When a new object is created using the 'new' keyword, Java automatically allocates the required memory for the object and its attributes. The exact amount of memory depends on the types of the object's attributes, and this low-level detail is abstracted away from the developer.
The memory in Java is mainly divided into two areas - stack and heap. The heap is where all the objects reside. Whenever an object is created using 'new', memory is allocated on the heap. The stack stores primitive types and references to objects in the heap, along with call stack frames.
The magic happens when it comes to memory freeing or garbage collection. Java has a built-in garbage collector that automatically recycles memory. When an object becomes unreachable (for example, when the references to it go out of scope or are set to null), Java determines it's no longer needed and its memory is marked as ready for garbage collection. The garbage collector then frees this memory automatically. The developer generally doesn't have to worry about when this happens, although for special cases, you can provide hints to the system about when to run garbage collection using methods like 'System.gc()'.
Therefore, one of the key features of Java is how it removes the burden of manual memory management from the developer, reducing the chance of issues like memory leaks and dangling pointers that are common in languages that require manual memory management.
The 'synchronized' keyword in Java is used as a means of concurrency control in multithreaded code. It helps prevent race conditions by ensuring that only one thread can access the synchronized method or block at a time.
When a method is declared with the 'synchronized' keyword, a thread must obtain the intrinsic lock on the object that the method belongs to before it can execute the method. If another thread is currently executing a synchronized method on the same object, the thread will block until the lock is released.
You can also use the 'synchronized' keyword to create a synchronized block within a method. In this case, you specify the object that the lock applies to. Only one thread at a time can execute a synchronized block on the same object.
Without synchronization, you could have problems where multiple threads are reading and writing shared state at the same time, leading to inconsistent results or other threading issues.
While this keyword is a simple way to achieve thread safety, it should be used with care. Overuse can lead to thread contention, where multiple threads continuously compete for the lock, causing your program to slow down or even deadlock, where threads block each other from progressing. Alternatives such as the 'java.util.concurrent' package provides more advanced and efficient concurrency utilities such as Locks, Semaphores and Atomic variables.
The Java Collections Framework is a set of classes and interfaces that provides a standard architecture for handling sets of data as a single unit, like lists, sets, queues, stacks, maps, and more.
Central to the framework are the core interfaces that each represent different types of collections:
The List interface represents an ordered collection of elements that can contain duplicates. Lists are great when sequence matters.
The Set interface represents a collection that cannot contain duplicate elements. This is useful when you want to store a set of unique elements, like a set of unique student IDs.
The Map interface represents a collection of key-value pairs, in which each key is unique. Maps are great for when you want to link values together and look them up by a specific key, just like a dictionary or a phonebook.
The Queue and Deque interfaces represent collections designed for holding elements prior to processing, following the classic data structures for handling things like "first in, first out".
These core collection interfaces are then implemented by several concrete classes like ArrayList, LinkedList, HashSet, TreeSet, HashMap, TreeMap, and so on, which provide the functionality to store and manipulate a collection of objects.
The Collections Framework also provides several utilities for manipulating and working with collections, like sorting, searching, shuffling, and more. It forms a key part of Java's standard library and is critical for handling data in just about any sort of application.
In Java, exceptions are handled using a try-catch-finally construct or by declaring the exception using the 'throws' keyword.
The 'try' block contains the set of statements where an exception can occur. It's followed by one or more 'catch' blocks, each designed to handle a particular type of exception. When an exception is thrown within a 'try' block, the program control is transferred to the corresponding 'catch' block. The role of the 'catch' block is to process the exception in a meaningful manner so that the program can continue, or at least fail gracefully.
Additionally, there's a 'finally' block. The code inside 'finally' is always executed, irrespective of whether an exception was thrown in the try block or not. This is typically used for cleaning up - closing files, network connections, or databases, so that resources are not left open.
Another way to handle exceptions is to declare it in the method definition using the 'throws' keyword. This is more of a "passing the buck" approach, you're indicating that your method may possibly throw this exception, so whoever calls this method is responsible for handling it.
Effective exception handling in Java is an art. It's important to handle only those exceptions that you can meaningfully recover from, and to pass the rest up to a level where they can be properly handled. Also, it's good practice to provide as much detailed information as possible when throwing exceptions, such as setting an informative message, to aid in debugging.
In Java, 'this' is a reference variable that refers to the current object. It provides an easy way to refer to the properties or methods of the current object from within its instance methods or constructors.
For instance, inside a class method or constructor, 'this' is often used to reference instance variables when they have the same name as method parameters or local variables. It helps differentiate the instance variables from local ones. So, if you have a class with a variable 'name', and you want to set that in a constructor that has a parameter also called 'name', you would use 'this.name = name' to clarify you're referring to the instance variable rather than the parameter.
'this' can also be used to call one constructor from another within the same class (constructor chaining), i.e., 'this()' or 'this(parameters)'.
Furthermore, 'this' can be passed as an argument to another method or used as a return value. 'this' can thus be used to achieve a variety of different effects and lend to clearer and more concise code.
Garbage collection in Java is one of the language's most beneficial features, as it manages the automatic recovery of memory in your programs when objects are no longer in use. This helps to prevent memory leaks that otherwise might occur if developers had to manually allocate and deallocate memory, like in some other languages.
Garbage collection is handled in the background by the Java Virtual Machine (JVM). When an object is created, it takes up memory. Once there are no more references to that object (meaning it can no longer be accessed by your code), it's a candidate for garbage collection. The JVM, at its discretion, recovers this memory by deleting these unreachable objects. The memory can then be reused for new objects.
As a developer, you don't have direct control over when garbage collection occurs. It's generally taken care of automatically by the JVM based on factors like available memory. However, you can make a request to the JVM to run garbage collector using 'System.gc()', but it's only a request and not guaranteed.
What you can control is making an object eligible for garbage collection. Basically, whenever you're done with an object, ensure it doesn't have any remaining references. This would involve nullifying references or letting variables go out of context once their use is done. Remember, effective memory management is still critical in Java to keep your applications running smoothly and efficiently.
In Java, an inner class or nested class is a class that is declared inside another class or interface. These classes are part of the outer class and can access its private data members and methods. Based on their declaration and behavior, inner classes can be categorized into four types.
Member Inner Classes: These are the inner classes that are defined at the same level as instance methods of the outer class. They can access both static and non-static members of the outer class.
Static Nested Classes: These are defined at the same level as static members of the outer class and they can't access non-static members of the outer class, unless they have an object reference for the outer class.
Local Inner Classes: These are defined inside a block, usually a method body. They can't be declared public, private, or protected, and they can only access final variables in the scope they are defined.
Anonymous Inner Classes: These are declared without a name and usually within a method body. They are either subclasses of a particular object or implement interfaces with a certain method, and as such are used primarily in GUI event handling.
Using inner classes can increase the encapsulation of your code and can lead to more readable and maintainable code as it more closely models the real-world objects you're trying to represent.
Swing and AWT (Abstract Window Toolkit) are both used for creating Graphical User Interfaces (GUI) in Java, but they do have some key differences.
AWT was the original Java GUI toolkit, introduced in the first version of Java Development Kit (JDK). It provides a number of simple GUI components like buttons, checkboxes, and text fields. It also includes layouts for organizing components in windows, graphics and font handling. One key characteristic of AWT is that it's a heavyweight toolkit, meaning that its components rely on peer components of the underlying platform's graphical user interface toolkit. This implies that the look and feel of AWT components can vary between platforms, as they're rendered using the underlying platform's GUI system.
Swing is actually a part of AWT, released with Java Foundation Classes (JFC) in Java 2 Standard Edition (J2SE). Swing is lightweight (doesn't rely on native GUI toolkit of underlying platform) and it provides a richer set of components like trees, tables, and lists. A consistent look and feel is maintained across all platforms, since Swing components are rendered by Java itself, not by the underlying platform's GUI system.
While AWT components are simpler and easier to use, Swing provides a larger, more flexible toolkit with greater customization options. Despite this, modern Java development has shifted towards JavaFX for new GUI application development, which provides a more modern and comprehensive approach to building desktop applications.
Efficient handling of large data sets in Java can be achieved via several strategies.
Firstly, using the right data structures is crucial. For instance, if you require quick searching, a HashSet might be a more appropriate choice than a list. If you require ordering, then a TreeMap or TreeSet could be considered. These data structures are part of Java's Collections Framework, which provides a variety of data structures optimized for different use cases.
Secondly, using streaming APIs can considerably reduce memory usage. The Stream API introduced in Java 8 helps to process collections of objects in a functional manner. They allow for operations to be performed on large datasets in sequence (or in parallel), without loading the entire dataset into memory.
Thirdly, efficient handling of large data sets is a prime use case for databases and Java provides robust capabilities for interacting with databases, including JDBC and various Object-Relational Mapping (ORM) tools like Hibernate.
Lastly, consider using multi-threading to efficiently handle large data set. Parallel processing using multiple threads can help break down operations on large data into smaller tasks, which can be executed concurrently to improve the performance.
It's crucial to profile and test different approaches with representative data sets to ensure you are using the most efficient practices for your particular use case. Chances are good that Java provides a feature or library that can help optimize your program's performance when working with large data sets.
Java Database Connectivity (JDBC) is used in Java to connect with databases to perform create, retrieve, update and delete (CRUD) operations.
To use JDBC, first, you'd need to establish a connection to the database using a JDBC driver. This is done by using DriverManager’s getConnection method, which requires a database URL, a username, and a password.
Sample code would look something like this: Connection conn = DriverManager.getConnection(dbUrl, userName, password);
Once a connection is established, you create a Statement object using the Connection, like so: Statement stmt = conn.createStatement();
Then you can execute SQL queries. For a SELECT query, you'd use: ResultSet rs = stmt.executeQuery("SELECT * FROM table");
You then process the ResultSet by iterating over it and reading the values.
For INSERT, UPDATE, or DELETE queries, you'd use: int rowsAffected = stmt.executeUpdate("INSERT INTO table VALUES (...)");
Finally, always remember to close the connection, statement, and result set objects to free up resources, ideally in a 'finally' block to ensure they run regardless of exception occurrences.
This is just a simple overview. Real-world uses often involve techniques like connection pooling, prepared statements, and transaction management for efficient and secure database operations. It's also a common practice to use an ORM tool like Hibernate which abstracts away much of the low level details and allows you to interact with your database in a more Java-centric way.
The Java ClassLoader plays a core role in the operation of the JVM, responsible for locating, loading, and initializing classes in a Java application.
The ClassLoader works in three primary steps: loading, linking, and initialization. When the JVM requests a class, the ClassLoader tries to locate the bytecode for that class, typically by looking in the directories and JAR files specified in the CLASSPATH. This is the loading phase.
In the linking phase, the loaded class is verified, ensuring that it is properly formed and does not contain any problematic instructions. Any variables are also allocated memory in this phase.
Finally, in the initialization phase, the static initializers for the class are run. These are any static variables and the static block, if one is present.
Java uses a delegation model for classloaders. When a request to load a class is made, it's passed to the parent classloader. If parent classloader doesn't find the class, then the classloader itself tries to load the class. Three class loaders are built into the JVM: Bootstrap (loads core Java classes), Extension (loads classes from extension directory), and System (loads code found on java.class.path).
Understanding class loaders and their hierarchy model is especially important when dealing with larger applications and systems, such as application servers, which involve many class loaders and require careful handling of classes and resources to avoid conflicts.
The Java Virtual Machine (JVM) plays an exceptionally vital role in Java development, and understanding how it works is key to becoming a proficient Java developer.
Firstly, JVM is responsible for running the Java byte-code, which is created after the Java compiler translates Java source code into this intermediate form. JVM executes the byte code line by line, thus providing the platform-independence that is a hallmark of Java - "write once, run anywhere".
Secondly, JVM is in charge of handling memory management through its Garbage Collector (GC). GC automatically deallocates memory that is no longer referenced, thus avoiding potential memory leaks, and developers do not have to manage memory manually.
Additionally, JVM also oversees other aspects such as handling exceptions, initiating objects, loading and linking necessary classes at runtime, and, crucially, coordinating multithreading and synchronization.
Without JVM, Java wouldn't have its appeal of platform independence, automatic memory management, and robust execution environment. It's the JVM that ensures the same Java byte-code can run seamlessly on a variety of hardware and software platforms.
Java provides four different access specifiers to set the visibility and accessibility of classes, methods, and other member variables. These are public, protected, private, and package-private (default).
Public: A public class, method, or field is visible to all other classes in the Java environment. That's why main methods are typically public, as they need to be accessible from outside the class when the program starts.
Private: A private field, method, or constructor is only visible within its own class. If you try to access it from elsewhere, the code won't compile. Private is often used to ensure that class implementation details are hidden and cannot be accessed by other classes.
Protected: A protected field or method is visible within its own package, like the default (package-private) level, but also in all subclasses, even if those subclasses are in different packages. This is often used to allow child classes to inherit properties or methods from a parent class.
Package-private (default): If you don't specify an access specifier, the default access level is used. A class, field, or method with default access is only visible within its own package. This is typically used when you want to restrict access to only the classes that are part of the same group, defined by the package.
Choosing the correct access modifier is an important aspect of object-oriented design as it helps in achieving encapsulation, one of the fundamental principles of object-oriented programming. A well-designed class will enforce proper access control to its fields and methods, limiting exposure of its internals to just what's necessary and no more.
Serialization in Java is the process of converting an object's state into a byte stream, which can then be persisted to a disk, a database, or sent over a network. Likewise, deserialization is the process of converting this byte stream back into a copy of the original object.
To make a Java object serializable, you implement the java.io.Serializable interface, which is a marker interface (i.e., it has no methods).
For example, if you have a 'Person' class with 'name' and 'age' fields, and you want to serialize instances of this class, you'd have the class implement Serializable:
public class Person implements Serializable { private String name; private int age; /* getters and setters go here */ }
To actually serialize an object, you use ObjectOutputStream's 'writeObject' method. And to deserialize, you use ObjectInputStream's 'readObject' method. Always remember these operations are I/O operations that need to be properly handled or they can cause exceptions.
One thing to note is that serialization involves not just the object, but also the object's transitively accessible state. That means if your object has references to other objects, those get serialized too, unless they're marked as 'transient' or they themselves are not serializable.
Use serialization when you need to send an object's state over the network, or save it to persistent storage for later retrieval. However, be aware that serialization does have its trade-offs in terms of performance and security that need to be considered.
Java Annotations are metadata about the program embedded in the program itself. They provide information about the code they annotate, and can be used by the compiler for compile-time and deployment-time processing and even at runtime.
Annotations can be attached to packages, classes, methods, parameters, fields, local variables, etc. Some annotations are built into the Java language, such as '@Override' which indicates a method is intended to override a method in a superclass. '@SuppressWarnings' is another built-in annotation that tells the compiler to suppress specific warnings.
Annotations can also be used to provide metadata for use by frameworks, which examine classes and methods at runtime and use annotations to guide their behavior. For instance, in Spring Framework, '@Autowired' is used to automatically inject dependencies, and '@RestController' is a special kind of '@Component' used for creating RESTful web services.
Creating custom annotations involves defining an interface with the '@interface' keyword, and they can specify elements that can be assigned values, for example @MyAnnotation(value=3)
. Values for these elements can be provided while declaring the annotation.
Take note that annotation does not change the execution flow of a program as they are just a way to provide additional information about the program. For the annotation to actually do something, some piece of code must be processing the annotation to drive an action based on its existence and configuration.
Java 11, which is a long-term support (LTS) version from Oracle, brought in several new features and improvements over its predecessors.
One of the most significant additions in this release is the introduction of the 'var' keyword to lambda parameters. This allows you to declare the types of the lambda parameters explicitly, which can make your code more readable, particularly in complex streams or when method references are not applicable.
Java 11 introduced a new HTTP Client API that supports HTTP/1.1 and HTTP/2. It was designed to improve the overall performance of sending requests by a Java program to a server and can replace the older, less efficient HttpURLConnection API.
String class got a couple of new methods — 'isBlank', 'lines', 'repeat', and 'strip' which is a more Unicode-aware trim method.
Java 11 also removed the Java EE and CORBA modules from the Java SE Platform and the JDK. These modules were already deprecated in Java 9 and are now completely removed in Java 11.
On the JVM front, Java 11 introduced the Epsilon garbage collector, which is a no-op garbage collector — useful for performance testing or short-lived jobs.
Finally, the 'jshell' tool, launched in Java 9, has been improved in Java 11, providing developers a handy way to run a few lines of Java without setting up a full project. And the new 'jpackage' utility lets you create self-contained, installable applications, which means you can distribute a single binary with your application and its dependencies, including the JVM itself.
JRE (Java Runtime Environment) and JDK (Java Development Kit) are two fundamental components of Java, each having a distinct role.
The JRE is essentially the Java Virtual Machine (JVM) where your Java programs run on. It also includes core libraries and other necessary components to run Java applications and applets that have been written using the Java programming language. So, if your machine has JRE installed, you can run Java programs on it. However, you can't develop and compile Java programs simply using JRE.
The JDK, on the other hand, includes JRE along with other tools and software needed to develop Java applications. This includes the Java compiler (javac), debugger (jdb), archiver (jar), documentation generator (javadoc) and more. So, in simpler terms, the JDK is for developers who need to compile and debug Java code, while the JRE is for people who just want to run pre-compiled Java applications.
In the end, if you're just running a Java application, you only need the JRE. But if you're a developer creating Java programs, you need the JDK.
Comparable and Comparator are both interfaces provided by Java to sort objects. However, they are used in different scenarios and have different purposes.
The Comparable interface is used to define the natural order of objects of a given class. When a class implements Comparable, it needs to override the 'compareTo' method, which compares 'this' object with the specified object. 'compareTo' should return a negative integer, zero, or a positive integer as 'this' object is less than, equal to, or greater than the specified object. The Comparable interface is great for situations where you have control over the class's source code and you know that the sorting logic will be consistent throughout your application.
The Comparator interface, on the other hand, is used when you want to define multiple different possible ways to sort instances of a class. To use a Comparator, you define a separate class that implements the Comparator interface, which includes a single method called 'compare'. This method takes two objects to be compared rather than just one. Comparator can sort the instances in any way you want without asking the class to be sorted to implement any interface. It's especially useful when you do not have access to the source code of the class to be sorted or when you want to provide multiple different sorting strategies. For example, you might have a Book class and multiple Comparator classes to sort by title, by author, by publication date, etc.
Caching is a common technique used to speed up applications by storing frequently accessed data in memory so accessing it the next time becomes faster.
One simple way to implement caching in Java is by using a HashMap to store the frequently accessed data. If you look for data and it's in the HashMap (cache hit), you can return it directly. If it's not (cache miss), you can fetch it from the source, store it in the HashMap for future use, and then return it.
Java provides the java.util.concurrent.ConcurrentHashMap
that can be used for a thread-safe, high-performance cache. It provides the computeIfAbsent
method which atomically executes the provided mapping function for a specified key, only if the key is not already present or null.
On the other hand, Java also provide classes specifically designed for caching, such as java.util.WeakHashMap
and java.lang.ref.SoftReference
that store keys in a way that doesn't prevent garbage collection. When memory becomes tight, entries are cleared out of the cache.
Outside the standard Java libraries, there exist several feature-rich caching libraries, such as Google's Guava library and Caffeine, or standalone caching servers like Memcached or Redis. For example, Guava provides the LoadingCache class, which automatically fetches the value using a provided method if the key is not in the cache.
Optimally implementing a cache often involves designing policies to handle cache eviction when the cache gets full, as well managing cache consistency when the underlying data changes. This can become a complex area of system design, but it is often essential for achieving high performance in resource-intensive applications.
Introduced in Java 8, the 'default' keyword is used in the context of interface methods. Before Java 8, interfaces could only contain method signatures (abstract methods) and constants, but no implementation. Java 8 brought the capability for interfaces to contain method implementations in the form of 'default' and 'static' methods.
The 'default' keyword is used to provide a default implementation of a method in an interface. This feature was introduced to keep old interfaces compatible with new functionality.
Let's say, for example, you have an existing interface used by many classes and you want to add a new method. If you add a regular abstract method, you would need to alter all implementing classes and add implementation for this new method, which might not be practical. Instead, you could add this new method as a 'default' method with its default implementation. All existing code would continue to work, and classes can choose to override this default implementation if needed.
Here's an example of a default method:
```java interface MyInterface { // Regular abstract method double calculate(double value);
// Default method
default double sqrt(double value) {
return Math.sqrt(value);
}
} ```
In the above interface, 'calculate' is a normal abstract method that any class implementing 'MyInterface' would have to implement. 'sqrt', however, is a default method and provides a default implementation, so classes implementing 'MyInterface' can choose to ignore it. However, if needed, they can still override the default method.
The Optional class in Java 8 is a container object that may or may not contain a non-null value. It provides a clear and explicit way to signal that a value may be absent, and is a good way to avoid Null Pointer Exceptions.
Typically, methods that can return null should instead return an Optional to indicate the possibility that the value could be absent. For example, consider a method that fetches a user by id. If the user isn't found, null might be returned. Using Optional, the method could return an empty Optional instead.
Here's an example:
java
public Optional<User> getUserById(int id) {
// Fetch user and if not found, return Optional.empty()
...
// if found, wrap the user in an Optional and return.
return Optional.of(user);
}
When you use the method, you can use the isPresent()
method to check if a value is present, and get()
to retrieve it. But to fully utilize the power of Optional, you can also use other methods like orElse()
which provides a default value if the Optional is empty, or orElseThrow()
which throws a specified exception if the Optional is empty. Here's an example:
java
User user = getUserById(id).orElseThrow(() -> new UserNotFoundException(id));
By using Optional in this way, you make your code explicitly signal when a value may be absent and force the user of your method to handle the case when the value is not present, thus reducing the chance of encountering a NullPointerException.
'==' and 'equals()' are used to compare values in Java, but they're used in different scenarios and have different implications.
The '==' operator is used to compare primitives and objects, but it behaves differently in these two cases. For primitives, '==' checks if the values are equal. For instance, '5 == 5' will return true. When comparing objects, '==' checks for reference equality, meaning it checks whether two references point to the exact same object in memory. It doesn't compare the content of the objects.
The 'equals()' method, on the other hand, is for comparing the content of objects. When you call 'equals()' on an object, it checks whether the content inside the object is the same as the content inside another object. Note though, the default implementation of 'equals()' in the Object class is essentially '==', so to have 'equals()' do a content comparison, the class needs to override this method with an appropriate definition. Many classes like String, Integer, Date, etc. in the Java library do this.
As a rule of thumb, if you want to compare the value of two primitives, use '=='. If you want to compare whether two objects are exactly the same object, use '=='. If you want to compare the contents or values of two objects, use 'equals()', assuming the class of those objects has an appropriate definition of 'equals()'. It's important to understand these distinctions to avoid unexpected behavior in code, especially when working with collections that use 'equals()' for operations like contains, remove, etc.
Java bytecode is the intermediate representation of Java code, which is produced by the Java compiler from .java source files. Bytecode files have a .class extension and are designed to be run by the Java Virtual Machine (JVM).
When you compile your Java code using the 'javac' command, the compiler transforms your high-level Java code to bytecode, which is a lower-level format. Bytecode is platform-independent, meaning it can be executed on any device as long as that device has a JVM installed. This gives Java its "write once, run anywhere" property.
At runtime, either the JVM interpreter executes this bytecode directly (interpreting it into instructions and executing those on the host machine) or the Just-In-Time (JIT) compiler compiles it further into native machine code for the host machine for better performance.
So essentially, Java bytecode enables the Java code to be portable and to be executed on any hardware platform which has a JVM. This level of abstraction separates the Java applications from the underlying hardware.
Lambda expressions and functional interfaces are key features introduced in Java 8 that enable functional programming style within Java, and they bring several advantages.
Lambda expressions are anonymous functions that you can use to create delegates or type-safe function pointers. They can be passed around as if they were objects and executed on demand. This allows you to write more concise and readable code by writing out the implementation of a function right at the place where it's used.
A functional interface in Java is an interface that contains only one abstract method, though it can have any number of default or static methods. The main purpose of a functional interface is to be used in context of lambda expressions. Lambdas can be used to instantiate objects of functional interface type.
By using lambda expressions with functional interfaces, you can encapsulate single units of behavior and pass it around. For example, you might pass a lambda expression that performs a calculation to a method which executes that calculation on a list of values. This can make your code more versatile and reusable.
Furthermore, they make it easier to work with APIs like the Stream API, making operations on collections more readable and expressive. For instance, instead of using a for-each loop to iterate through a collection and perform an action on each item, you can use the 'forEach' method with a lambda expression that defines the action to perform.
In sum, the combination of lambda expressions and functional interfaces can lead to more compact, readable and maintainable code, while enabling functional programming patterns in Java.
There is no better source of knowledge and motivation than having a personal mentor. Support your interview preparation with a mentor who has been there and done that. Our mentors are top professionals from the best companies in the world.
We’ve already delivered 1-on-1 mentorship to thousands of students, professionals, managers and executives. Even better, they’ve left an average rating of 4.9 out of 5 for our mentors.
"Naz is an amazing person and a wonderful mentor. She is supportive and knowledgeable with extensive practical experience. Having been a manager at Netflix, she also knows a ton about working with teams at scale. Highly recommended."
"Brandon has been supporting me with a software engineering job hunt and has provided amazing value with his industry knowledge, tips unique to my situation and support as I prepared for my interviews and applications."
"Sandrina helped me improve as an engineer. Looking back, I took a huge step, beyond my expectations."
"Andrii is the best mentor I have ever met. He explains things clearly and helps to solve almost any problem. He taught me so many things about the world of Java in so a short period of time!"
"Greg is literally helping me achieve my dreams. I had very little idea of what I was doing – Greg was the missing piece that offered me down to earth guidance in business."
"Anna really helped me a lot. Her mentoring was very structured, she could answer all my questions and inspired me a lot. I can already see that this has made me even more successful with my agency."