10 Common Questions about Java Fundamentals

These are questions that will never be outdated no matter how things change

JackyNote ⭐️
13 min readOct 10, 2023

After every interview I participate in, I always save the technical questions as notes, and re-read them to remember knowledge. So today, I want to share them so everyone can read and best prepare for their interview.

1. Enum in Java

In Java, an enum (short for enumeration) is a special data type that allows you to define a collection of constants. It is used to represent a fixed set of predefined values or options. Enumerations in Java are declared using the enum keyword.

enum.name() vs enum.toString() vs Enum.valueOf()

In Java, the both are used to obtain a string representation of an enum constant, but they are not exactly the same.

enum.name() is an instance method defined in the Enum class, returns the name of the enum constant as declared in the enum definition.

enum.toString() also returns the name of the enum constant, just like enum.name().

However, the behavior of enum.toString() can be overridden for each enum constant if you want to provide a custom string representation for that constant.

Enum.valueOf() is a static method defined in the Enum class, and it is used to retrieve an enum constant from its name as a string.

In summary, Enum.valueOf() is used to get the enum constant by providing its name as a string, while enum.name() is used to obtain the name of an enum constant that the method is called on. Both methods can be helpful depending on the scenario you are dealing with.

2. String in Java

In Java, String is a class that represents a sequence of characters. It’s is immutable and you can not be changed content of String.

2.1. == vs equals()

In Java, the == operator is used to compare the object reference of two objects, while the equals() method is used to compare the contents of two objects. For example:

String s1 = "hello";
String s2 = "hello";
String s3 = new String("hello");

System.out.println(s1 == s2); // true
System.out.println(s1 == s3); // false
System.out.println(s1.equals(s2)); // true
System.out.println(s1.equals(s3)); // true

In the example above, s1 and s2 are both references to the same string in memory, so s1 == s2 is true. s3 is a new string object with the same contents as s1, so s1 == s3 is false. However, s1.equals(s2) and s1.equals(s3) are both true, because equals() compares the contents of the strings.

2.2. String vs StringBuilder

In Java, String objects are immutable, which means that once a String object is created, its contents cannot be changed. If you need a mutable string, you can use the StringBuilder class instead. For example:

StringBuilder sb = new StringBuilder("hello");
sb.append(" world");
String s = sb.toString(); // "hello world"

2.3. concat()

The concat() method can be used to concatenate two strings together. For example:

String s1 = "hello";
String s2 = " world";
String s3 = s1.concat(s2); // "hello world"

2.4. StringPool in Java

In Java, the StringPool (also known as the String Constant Pool) is a special memory area in the Java Virtual Machine (JVM) that stores a pool of unique string literals. It is a mechanism used to optimize memory and improve the performance of string handling in Java applications.

In Java, when you create a string using double quotes (“…”) and assign it to a variable, the JVM automatically checks the StringPool to see if an identical string already exists in the pool. If string content exists, they will reference into variable, if not, they will created new object.

It ensures that multiple references to the same string literal point to the same memory location, saving memory and reducing object creation overhead.

String str1 = "Hello"; // A new string "Hello" is created and added to the StringPool.
String str2 = "Hello"; // JVM looks in the StringPool and finds an existing "Hello" string, so str2 points to the same location as str1.

Using the StringPool can be beneficial in scenarios where you have a large number of identical or similar string literals in your code, as it helps to reduce memory consumption and improve performance by minimizing redundant string objects.

2.5. StringBuffer vs StringBuilder

In Java, both StringBuffer and StringBuilder are mutable string classes. The difference is that StringBuffer is thread-safe, while StringBuilder is not.

If you need to modify a string in a multi-threaded environment, you should use StringBuffer. Otherwise, you should use StringBuilder.

In summary, String is a class that represents a sequence of characters in Java. It is important to know the differences between == operator and equals()method, as well as to understand the differences between String and StringBuilder or StringBuffer.

3. Thread Safety in Java

Thread safety is an important concept in Java programming, especially in multi-threaded environments. It refers to the ability of a program to function correctly and consistently when multiple threads are executing parallel.

In Java, there are several ways to achieve thread safety. One way is to use synchronized blocks or methods to ensure that only one thread can access a particular block of code at a time. For example:

public synchronized void incrementCounter() {
counter++;
}

In the example above, the synchronized keyword ensures that only one thread can execute the incrementCounter() method at a time. This helps to prevent race conditions and other concurrency issues.

Another way to achieve thread safety in Java is to use thread-safe data structures. For example, the java.util.concurrent package provides several thread-safe collections, such as ConcurrentHashMap and ConcurrentLinkedQueue.

It is important to note that achieving thread safety in Java can come at a performance cost. Synchronization and other thread-safety measures can introduce overhead and slow down the program’s execution. Therefore, it is important to carefully consider the trade-offs between thread safety and performance when designing and implementing a Java program.

4. Functional Interfaces in Java

A functional interface in Java is an interface that has only one abstract method. This means that a functional interface can be look like as a lambda expression or method reference.

Functional interfaces are used in Java 8 and later versions to implement lambda expressions and method references.

For example, the java.util.function package in Java 8 defines several built-in functional interfaces, such as Predicate, Consumer, and Function.

Here is an example of a simple functional interface that takes two integers and returns their sum:

@FunctionalInterface
public interface Popo {
int sum(int a, int b);
}

In this example, the IntSum interface has only one abstract method, sum(), which takes two integers as arguments and returns their sum.

This interface can be used to create lambda expressions that implement the sum() method. For example:

Popo popo = (a, b) -> a + b;
int result = popo.sum(5, 10); // result = 15

In this example, the lambda expression (a, b) -> a + b implements the sum() method defined in the IntSum interface.

The lambda expression is then assigned to a variable of type IntSum, which can be used to call the sum() method with different arguments.

Functional interfaces are a powerful feature in Java that allow for concise and flexible code. They are often used in conjunction with lambda expressions and method references to implement functional programming concepts in Java.

5. Stack vs Heap in Java

Stack vs Heap in Java

What is the Stack?

The stack is a region of memory where method invocations and local variables are stored. Each time a method is called, a new frame is added to the stack. When the method finishes executing, its frame is removed from the stack.

What is the Heap?

The heap is a region of memory where objects are stored. When an object is created, it is allocated on the heap. The object remains on the heap until it is no longer referenced by any other objects or variables.

Java’s garbage collector is responsible for identifying and freeing unused objects in the Heap.

What are the differences between Stack and Heap?

  • The stack is used for method invocations and local variables, while the heap is used for object allocation.
  • The stack is much smaller than the heap.
  • The stack is organized in a Last-In-First-Out (LIFO) manner, while the heap is not.
  • Access to the stack is much faster than access to the heap.

In summary, the stack and the heap are two different regions of memory used in Java. The stack is used for method invocations and local variables, while the heap is used for object allocation. Access to the stack is faster than access to the heap, but the heap can store much larger amounts of data.

6. Garbare Collection

In Java, the garbage collector is responsible for identifying and freeing unused objects in the heap. The garbage collector runs in the background, periodically checking the heap for objects that are no longer being used. When it finds an object that is no longer being referenced by any other objects or variables, it marks the object as eligible for garbage collection. The garbage collector then frees up the memory occupied by the object, making it available for future use.

Java’s garbage collector uses a mark-and-sweep algorithm to identify and free unused objects. During the mark phase, the garbage collector traverses the object graph, starting from the roots (such as static variables and method parameters), marking all objects that are still being used. During the sweep phase, the garbage collector frees up the memory occupied by objects that were not marked during the mark phase.

There are several different garbage collectors available in Java, each with its own strengths and weaknesses. The most commonly used garbage collector is the ParallelGC collector, which is designed for use on machines with multiple processors.

In summary, the garbage collector in Java is responsible for freeing up memory occupied by unused objects in the heap. It uses a mark-and-sweep algorithm to identify and free unused objects, and there are several different garbage collectors available with different strengths and weaknesses.

Why GC is very important in Java?

GC can have unpredictable effects on Java application performance. When GC activity occurs a lot, it adds a lot of load to the CPU and slows down the application processing, resulting in slow transaction execution and ultimately affecting the user experience.

Excessive GC activity can be caused by a memory leak in the Java application, or by the programmer not allocating enough memory for the JVM. Usually a sign of GC being overworked is a high CPU usage of the JVM.

To have the Java application achieve optimal performance, we must monitor the GC activity of the JVM. For good performance, GC should run at low frequency only.

How to enabled GC?

Enable garbage collection logs to observe how the GC execute in your application. You can enable GC logging by adding command-line flags when starting the JVM

7. Collections in Java

In Java, Collections is a class that provides various utility methods for working with collections of objects, such as List, Set, and Map.

The contains() method is a method defined in the Collection interface, which is implemented by several collection classes in Java, such as ArrayList and HashSet.

The contains() method is used to check whether a collection contains a specific element. For example:

List<String> names = new ArrayList<>();
names.add("Alice");
names.add("Bob");
names.add("Charlie");

boolean containsBob = names.contains("Bob"); // true
boolean containsDave = names.contains("Dave"); // false

In the example above, the contains() method is used to check whether the names list contains the elements "Bob" and "Dave".

The method returns true if the collection contains the specified element, and false otherwise.

In summary, the contains() method is a method defined in the Collection interface in Java, and it is used to check whether a collection contains a specific element.

7.1. List vs Set

In Java, both List and Set are interfaces that are part of the Java Collections Framework, which provides a set of classes and interfaces to work with collections of objects. Both List and Set are used to store collections of elements, but they have some key differences:

7.2. ArrayList vs LinkedList

ArrayList and LinkedList are both implementations of the List interface in Java. They are used to store collections of elements, but they have different underlying data structures and performance characteristics, which make them suitable for different use cases:

7.3. List vs Map

List

A List is an ordered collection of elements that allows duplicate values. It is implemented by classes like ArrayList, LinkedList, and Vector. Elements in a List have an index, and you can access elements by their index. List supports various methods for adding, removing, and accessing elements by their index. Examples of usage include maintaining a collection of objects in a specific order or creating a sequence of elements.

Map

A Map is a collection that stores key-value pairs, where each key is unique, and each key maps to exactly one value. It is implemented by classes like HashMap, TreeMap, and LinkedHashMap. Elements in a Map are not ordered by their insertion sequence; instead, they are ordered based on the key’s natural ordering (for TreeMap) or the insertion order (for LinkedHashMap). Map supports methods to put, get, and remove elements based on the keys. Examples of usage include creating a dictionary, mapping unique identifiers to corresponding objects, or counting occurrences of elements in a dataset.

8. OOP Principals

OOP (Object-Oriented Programming) principles, also known as OOP concepts, are fundamental concepts that guide the design and implementation of object-oriented software. These principles help developers create code that is easier to understand, maintain, and extend. The four main OOP principles are:

8.1 Encapsulation

Encapsulation involves bundling data and methods within a single unit (class) and hiding the internal details from the outside world. Example, let’s create a class Animal that represents different animals in the zoo. We'll encapsulate the animal's name and age within the class and provide methods to interact with this data.

public class Animal {
private String name;
private int age;

public Animal(String name, int age) {
this.name = name;
this.age = age;
}

public String getName() {
return name;
}

public int getAge() {
return age;
}

public void makeSound() {
// Method to make the animal sound.
}
}

In this example, we encapsulate the name and age attributes as private, preventing direct access from outside the class. The only way to access this data is through the getter methods getName() and getAge(). The makeSound() method is also encapsulated within the class.

8.2. Inheritance

Inheritance allows us to create a new class (subclass) based on an existing class (superclass), inheriting its attributes and methods. Let’s create two subclasses of Animal: Lion and Elephant.

public class Lion extends Animal {
public Lion(String name, int age) {
super(name, age);
}

@Override
public void makeSound() {
System.out.println("Roar!");
}
}

public class Elephant extends Animal {
public Elephant(String name, int age) {
super(name, age);
}

@Override
public void makeSound() {
System.out.println("Trumpet!");
}
}

Here, Lion and Elephant inherit the name and age attributes from the Animal class. Each subclass also overrides the makeSound() method to provide its specific implementation.

8.3. Polymorphism

Polymorphism allows objects of different classes to be treated as objects of a common superclass. We can use the Animal class to create objects of Lion and Elephant and call their respective methods.

public class Zoo {
public static void main(String[] args) {
Animal lion = new Lion("Leo", 5);
Animal elephant = new Elephant("Ellie", 10);

lion.makeSound(); // Output: Roar!
elephant.makeSound(); // Output: Trumpet!
}
}

In this example, we can treat both lion and elephant as Animal objects and call the makeSound() method. The actual implementation of makeSound() depends on the type of object (runtime polymorphism).

8.4. Abstraction

Abstraction allows us to define the common characteristics and behaviors of a class without specifying the complete implementation. In our Animal class, we define the makeSound() method as an abstraction of the animal's sound, but we don't specify the exact sound in the Animal class. It's left to the subclasses (Lion and Elephant) to provide their specific implementations.

9. hashcode() vs equals()

In Java, hashcode is a method defined in the Object class.

By default, the hashcode method returns an integer value based on the memory address of the object. However, many classes override this method to provide a more meaningful hash code based on the object's content or attributes.

The main purpose of a hash code is to support hash-based collections, such as HashMap, HashSet, and Hashtable. These collections use hash codes to quickly locate and organize objects for efficient retrieval.

The equals method is used to check if two objects are "equal" based on their content or state, not on their memory addresses.

10. Stream API in Java

The Stream API in Java 8 is a strong addition that helps with functional-style actions on groups of things, like collections (e.g., List, Set, Map) and arrays. we can do different actions on these groups using functional programming ideas, which makes the code shorter, easier to understand, and possibly faster.

Intermediate Operations

Intermediate operations are used to transform or filter the elements in the stream. Some common intermediate operations include map, filter, sorted, distinct, limit, and skip.

Terminal Operations

Terminal operations are used to produce a final result or side effect. Once a terminal operation is called, the stream is consumed and cannot be reused. Common terminal operations include forEach, collect, reduce, count, min, max, anyMatch, allMatch, and noneMatch.

Parallel Streams

The Stream API allows you to create parallel streams using the parallelStream() method. Parallel streams utilize multiple threads to process the elements concurrently, potentially speeding up processing for large datasets.

Thanks for reading

  • 👏 Please clap for the story and follow me 👉
  • 📰 Read more content on my Medium (Life of Java Developer)
  • 🔔 Follow me: LinkedIn | Twitter

--

--

JackyNote ⭐️

🚀 Software Engineer | Full Stack Java 7 Years of Experience | Tech Enthusiast | Startup Lover | Coffee