In this article, we will explore the top 10 most common mistakes in Java and how to avoid them with bad and good code examples.
1️⃣ Using ==
Instead of .equals()
for String Comparison
One of the most common mistakes in Java is comparing Strings using ==
instead of .equals()
. The ==
operator checks for reference equality, while .equals()
checks for value equality.
❌ Bad Code:
String str1 = new String("Java");
String str2 = new String("Java");
if (str1 == str2) { // WRONG: Checks reference, not value
System.out.println("Strings are equal");
} else {
System.out.println("Strings are not equal");
}
🔴 Output:
Strings are not equal
✅ Good Code:
String str1 = "Java";
String str2 = "Java";
if (str1.equals(str2)) { // CORRECT: Checks actual value
System.out.println("Strings are equal");
}
🟢 Output:
Strings are equal
2️⃣ Forgetting to Override hashCode()
When Overriding equals()
In Java, when you override equals()
, you must also override hashCode()
. Otherwise, collections like HashSet, HashMap, and HashTable won’t function properly.
❌ Bad Code (Only Overriding equals()
):
class Employee {
String name;
Employee(String name) {
this.name = name;
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (!(obj instanceof Employee)) return false;
Employee emp = (Employee) obj;
return name.equals(emp.name);
}
}
public class Main {
public static void main(String[] args) {
Employee e1 = new Employee("Alice");
Employee e2 = new Employee("Alice");
System.out.println(e1.equals(e2)); // true
HashSet<Employee> employees = new HashSet<>();
employees.add(e1);
System.out.println(employees.contains(e2)); // false ❌ Wrong!
}
}
✅ Good Code (Override Both equals()
and hashCode()
):
import java.util.Objects;
class Employee {
String name;
Employee(String name) {
this.name = name;
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (!(obj instanceof Employee)) return false;
Employee emp = (Employee) obj;
return name.equals(emp.name);
}
@Override
public int hashCode() {
return Objects.hash(name); // Correctly override hashCode
}
}
How Does This Fix the Issue?
- Now,
e1.hashCode()
ande2.hashCode()
will return the same value because they are based on thename
field. - When calling
contains(e2)
, theHashSet
looks in the correct bucket and findse1
, returning true.
📌 Summary:
- Always override
hashCode()
when overridingequals()
to ensure correct behavior in hash-based collections. - If two objects are considered equal by
equals()
, they must have the samehashCode()
. - Not overriding
hashCode()
can cause unexpected behavior, likeHashSet.contains()
returningfalse
for an object that is logically "equal" to one already in the set.
🚀 Rule of Thumb: If an object will be used as a key in HashSet
, HashMap
, or similar collections, override both equals()
and hashCode()
properly.
3️⃣ Not Closing Resources (Memory Leaks!)
Failing to close database connections, file readers, or network sockets can lead to memory leaks and performance issues.
❌ Example 1: Not Closing a File Resource
import java.io.*;
public class FileReaderExample {
public static void main(String[] args) throws IOException {
FileReader fr = new FileReader("file.txt");
BufferedReader br = new BufferedReader(fr);
System.out.println(br.readLine());
// ❌ Resource not closed! Can cause memory leak
}
}
✅ Good Code (Using try-with-resources
)
import java.io.*;
public class FileReaderExample {
public static void main(String[] args) {
try (BufferedReader br = new BufferedReader(new FileReader("file.txt"))) {
System.out.println(br.readLine());
} catch (IOException e) {
e.printStackTrace();
}
}
}
✔️ Why Is This Better?
- The
BufferedReader
is automatically closed when thetry
block ends. - No need to manually call
br.close()
. - Even if an exception occurs, the resource is closed properly.
❌ Example 2: Not Closing a Database Connection
import java.sql.*;
public class DatabaseExample {
public static void main(String[] args) throws SQLException {
Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/db", "user", "password");
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("SELECT * FROM employees");
while (rs.next()) {
System.out.println(rs.getString("name"));
}
// ❌ Connection, Statement, and ResultSet are not closed!
}
}
🚨 Problem:
- The database connection is left open, which can exhaust database resources.
✅ Fixed Code: Using try-with-resources
import java.sql.*;
public class DatabaseExample {
public static void main(String[] args) {
try (Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/db", "user", "password");
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("SELECT * FROM employees")) {
while (rs.next()) {
System.out.println(rs.getString("name"));
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
✔️ Now, all resources are automatically closed! 🚀
❌ Example 3: Not Closing a Network Socket
import java.io.*;
import java.net.*;
public class SocketExample {
public static void main(String[] args) throws IOException {
Socket socket = new Socket("example.com", 80);
OutputStream out = socket.getOutputStream();
out.write("GET / HTTP/1.1\r\n".getBytes());
// ❌ Socket remains open!
}
}
🚨 Problem:
- The socket remains open, leading to network resource exhaustion.
✅ Fixed Code: Using try-with-resources
import java.io.*;
import java.net.*;
public class SocketExample {
public static void main(String[] args) {
try (Socket socket = new Socket("example.com", 80);
OutputStream out = socket.getOutputStream()) {
out.write("GET / HTTP/1.1\r\n".getBytes());
} catch (IOException e) {
e.printStackTrace();
}
}
}
🎯 Key Takeaways
✅ Always close resources like files, database connections, and sockets to prevent memory leaks.
✅ Use try-with-resources (try (...) {}
) to ensure automatic closure.
✅ Manually closing resources (.close()
) is error-prone and should be avoided.
✅ Unclosed resources can cause memory leaks, performance issues, and system crashes.
🚀 Rule of Thumb:
"If a class implements AutoCloseable
, always use try-with-resources!"
4️⃣ Catching Generic Exceptions (Poor Error Handling)
Catching generic exceptions (Exception
or Throwable
) makes debugging difficult because it can hide real issues.
❌ Bad Code (Catching Everything)
try {
int result = 10 / 0; // ArithmeticException
} catch (Exception e) { // ❌ Catches all exceptions
System.out.println("Something went wrong");
}
🔹 Issue: It swallows all exceptions, making debugging hard.
✅ Good Code (Catch Specific Exceptions)
try {
int result = 10 / 0;
} catch (ArithmeticException e) {
System.out.println("Cannot divide by zero!");
}
✔️ More informative and specific error handling.
5️⃣ Using nextInt()
Without Handling Newline Issue in Scanner
When using Scanner
to take integer and string inputs together, calling nextInt()
without handling the newline (\n
) left in the buffer can cause issues.
❌ Bad Code (Skipping Input)
import java.util.Scanner;
public class ScannerExample {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.print("Enter age: ");
int age = scanner.nextInt(); // Reads integer
System.out.print("Enter name: ");
String name = scanner.nextLine(); // ❌ Skips input
System.out.println("Name: " + name + ", Age: " + age);
}
}
🔴 Issue:
nextInt()
does not consume the newline character (\n
) after reading the integer.nextLine()
reads the leftover newline instead of waiting for user input.
✅ Good Code (Handling Scanner Issue)
import java.util.Scanner;
public class ScannerExample {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.print("Enter age: ");
int age = scanner.nextInt();
scanner.nextLine(); // ✔️ Consume the newline
System.out.print("Enter name: ");
String name = scanner.nextLine(); // ✔️ Now works correctly
System.out.println("Name: " + name + ", Age: " + age);
}
}
✔️ Adding scanner.nextLine();
after nextInt()
ensures proper input handling.
6️⃣ Using public
Fields Instead of Encapsulation
Encapsulation is one of the core principles of Object-Oriented Programming (OOP). It helps protect the internal state of an object and makes the class easier to maintain and modify in the future.
When you declare fields as public
, any part of the code can modify them directly, breaking encapsulation.
❌ Bad Code (Using Public Fields)
class User {
public String name;
}
public class Main {
public static void main(String[] args) {
User user = new User();
user.name = "Amit"; // ❌ Direct access to the field
System.out.println(user.name);
}
}
🔴 Issues:
- No control over modifications – Anyone can change the field without any validation.
- Difficult to add extra logic later – If you need to validate or log changes, you have to modify all usages of the field across the entire codebase.
- Breaks encapsulation – The internal details of the class are exposed, making it harder to maintain.
🟢 Good Code: Using Encapsulation
Encapsulation means:
- Making fields
private
so they cannot be accessed directly. - Providing controlled access through getter and setter methods.
✅ Encapsulated Code (Using Getters and Setters)
class User {
private String name; // 🔒 Private field
public String getName() { // ✅ Getter method
return name;
}
public void setName(String name) { // ✅ Setter method
this.name = name;
}
}
public class Main {
public static void main(String[] args) {
User user = new User();
user.setName("Amit"); // ✅ Controlled modification
System.out.println(user.getName()); // ✅ Controlled access
}
}
✔️ Why Is This Better?
- Protects data – Other classes cannot modify
name
directly. - Allows validation – You can add rules inside the setter, e.g., prevent empty names.
- Improves maintainability – Future modifications (e.g., logging, format checks) can be added without breaking existing code.
Example: Adding Validation in Setter
Encapsulation allows us to add validation logic easily.
✅ Encapsulation with Validation
class User {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
if (name == null || name.trim().isEmpty()) {
throw new IllegalArgumentException("Name cannot be empty!"); // ✔️ Validation logic
}
this.name = name;
}
}
public class Main {
public static void main(String[] args) {
User user = new User();
user.setName("Amit"); // ✅ Valid name
System.out.println(user.getName());
user.setName(""); // ❌ Throws exception (Prevents invalid data)
}
}
✔️ Now, invalid names are not allowed!
Example: Read-Only Fields (No Setter)
If a field should not be modified after initialization, you can omit the setter.
✅ Encapsulation with Read-Only Property
class User {
private final String name; // 🔒 Read-only field
public User(String name) { // ✅ Set only once via constructor
this.name = name;
}
public String getName() { // ✅ Getter only
return name;
}
}
public class Main {
public static void main(String[] args) {
User user = new User("Amit");
System.out.println(user.getName()); // ✅ Allowed
// user.setName("Ravi"); ❌ Not allowed (No setter)
}
}
✔️ Now, name
cannot be changed after the object is created.
🎯 Key Takeaways
✅ Always make fields private
and use getters and setters for controlled access.
✅ Encapsulation allows validation, ensuring that only valid data is stored.
✅ Read-only properties can be created by not providing a setter.
✅ Encapsulation improves security, maintainability, and flexibility in the code.
🚀 Rule of Thumb:
"Never expose class fields directly. Always use encapsulation to control data access!"
7️⃣ Using Iterator
Incorrectly (Concurrent Modification Exception)
Modifying a collection while iterating over it can lead to ConcurrentModificationException
.
❌ Bad Code (Direct Modification)
List<String> list = new ArrayList<>();
list.add("A");
list.add("B");
for (String item : list) {
if (item.equals("A")) {
list.remove(item); // ❌ ConcurrentModificationException
}
}
for-each
loop or for loop does not allow modification of the list while iterating.
- Results in
ConcurrentModificationException
at runtime.
✅ Good Code (Using Iterator
)
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
if (iterator.next().equals("A")) {
iterator.remove(); // ✔️ Safe removal
}
}
Iterator.remove()
ensures safe modification during iteration.8️⃣ Hardcoding Values Instead of Using Constants
Hardcoding values make the code harder to maintain.
❌ Bad Code (Hardcoded Values)
if (userRole == 1) { // ❌ What does 1 mean?
System.out.println("Admin Access");
}
✅ Good Code (Using Constants)
public static final int ROLE_ADMIN = 1;
if (userRole == ROLE_ADMIN) {
System.out.println("Admin Access");
}
✔️ Makes the code more readable and maintainable.
9️⃣ Not Using Generics (Type Safety Issues)
Using raw types instead of generics can cause ClassCastException.
❌ Bad Code (Without Generics)
List list = new ArrayList();
list.add("Java");
String s = (String) list.get(0); // ❌ Explicit cast needed
✅ Good Code (With Generics)
List<String> list = new ArrayList<>();
list.add("Java");
String s = list.get(0); // ✔️ No cast needed
✔️ Generics improve type safety and remove unnecessary casting.
🔟 Using String
for Heavy String Manipulation Instead of StringBuilder
Using String
in loops leads to unnecessary object creation.
❌ Bad Code (String
in Loop)
String result = "";
for (int i = 0; i < 1000; i++) {
result += i; // ❌ Creates a new object in each iteration
}
✅ Good Code (StringBuilder
)
StringBuilder result = new StringBuilder();
for (int i = 0; i < 1000; i++) {
result.append(i);
}
✔️ StringBuilder
is much more efficient for modifications.
🎯 Conclusion: Write Java Like a Pro!
Avoiding common Java mistakes is crucial for writing efficient, maintainable, and bug-free code. Whether you are a beginner or an experienced Java developer, keeping these best practices in mind will enhance performance, improve code readability, and prevent costly debugging efforts.
✅ Key Takeaways:
✔️ Use .equals()
instead of ==
for string comparison.
✔️ Override both equals()
and hashCode()
to avoid unexpected behavior in collections.
✔️ Always close resources properly using try-with-resources to prevent memory leaks.
✔️ Catch specific exceptions instead of using generic ones.
✔️ Using nextInt()
Without Handling Newline Issue in Scanner
✔️ Use encapsulation to protect data and maintain code structure.
✔️ Avoid modifying collections while iterating without using an Iterator
.
✔️ Use constants instead of hardcoded values for better readability.
✔️ Leverage generics to improve type safety and remove unnecessary casting.
✔️ Use StringBuilder
instead of String
for efficient string manipulation.
By following these best practices, you will write better Java code, prevent common pitfalls, and create robust applications. Adopt these habits today and see an improvement in your Java programming skills! 🚀
Comments
Post a Comment
Leave Comment