🔒 The Final Keyword in Java
The final
keyword, on the other hand, is used to create elements that cannot be changed once they are initialized. It can be applied to variables, methods, and classes, each with different effects.
What Does the Final Keyword in Java Mean?
When applied to different Java elements, final
has the following meanings:
- Final Variables: Cannot be reassigned after initialization
- Final Methods: Cannot be overridden by subclasses
- Final Classes: Cannot be extended (subclassed)
Let's explore each of these in detail.
🔐 Final Variables in Java
A final variable can only be assigned once. After initialization, its value cannot be changed. This applies to:
- Instance variables
- Static variables
- Local variables
- Method parameters
Basic Example:
public class FinalVariableDemo {
// Final instance variable - must be initialized in declaration or constructor
private final int instanceVar = 10;
// Final static variable (constant)
public static final double PI = 3.14159;
// Final instance variable initialized in constructor
private final String name;
public FinalVariableDemo(String name) {
this.name = name; // Initializing final variable in constructor
// this.instanceVar = 20; // Error: Cannot assign a value to final variable
}
public void demonstrateFinalVariables() {
// Final local variable
final int localVar = 5;
// localVar = 10; // Error: Cannot assign a value to final variable
// Final method parameter
demonstrateFinalParameter("Hello");
}
public void demonstrateFinalParameter(final String message) {
// message = "Changed"; // Error: Cannot assign a value to final parameter
System.out.println("Message: " + message);
// Final variable in a block
if (true) {
final int blockVar = 15;
System.out.println("Block variable: " + blockVar);
// blockVar = 20; // Error: Cannot assign a value to final variable
}
}
public static void main(String[] args) {
FinalVariableDemo demo = new FinalVariableDemo("Example");
demo.demonstrateFinalVariables();
System.out.println("PI: " + PI);
// PI = 3.14; // Error: Cannot assign a value to final variable
}
}
In this example, we have various types of final variables:
instanceVar
: A final instance variable initialized at declarationPI
: A final static variable (constant)name
: A final instance variable initialized in the constructorlocalVar
: A final local variablemessage
: A final method parameterblockVar
: A final variable in a block
None of these variables can be reassigned after initialization.
Final Reference Variables in Java
It's important to understand that when a reference variable is declared as final, the reference cannot be changed, but the object it refers to can still be modified.
public class FinalReferenceDemo {
public static void main(String[] args) {
// Final reference to a list
final List<String> names = new ArrayList<>();
// We can modify the list
names.add("Alice");
names.add("Bob");
names.add("Charlie");
System.out.println("Names: " + names);
// We can clear the list
names.clear();
System.out.println("Names after clear: " + names);
// But we cannot reassign the reference
// names = new ArrayList<>(); // Error: Cannot assign a value to final variable
// Final reference to a mutable object
final Person person = new Person("John", 30);
System.out.println("Person before: " + person);
// We can modify the object's state
person.setAge(31);
System.out.println("Person after: " + person);
// But we cannot reassign the reference
// person = new Person("Jane", 25); // Error: Cannot assign a value to final variable
}
static class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "Person{name='" + name + "', age=" + age + "}";
}
}
}
Output:
Names: [Alice, Bob, Charlie]
Names after clear: []
Person before: Person{name='John', age=30}
Person after: Person{name='John', age=31}
In this example, we have two final reference variables:
names
: A final reference to a list. We can add, remove, or clear elements in the list, but we cannot reassignnames
to a new list.person
: A final reference to a Person object. We can modify the person's age, but we cannot reassignperson
to a new Person object.
Common Use Cases for Java Final Variables:
- Constants: Values that should not change throughout the program
- Immutable Objects: Creating objects whose state cannot be changed after construction
- Thread Safety: Ensuring that variables shared between threads cannot be modified
- Functional Programming: Supporting lambda expressions and functional interfaces
Example with Java Constants:
public class ApplicationConstants {
// Application settings
public static final String APP_NAME = "MyApp";
public static final String APP_VERSION = "1.0.0";
public static final int MAX_USERS = 100;
// Database settings
public static final String DB_URL = "jdbc:mysql://localhost:3306/myapp";
public static final String DB_USERNAME = "admin";
public static final int DB_CONNECTION_TIMEOUT = 30000; // 30 seconds
// File paths
public static final String CONFIG_FILE_PATH = "/etc/myapp/config.properties";
public static final String LOG_FILE_PATH = "/var/log/myapp/app.log";
// Error messages
public static final String ERROR_CONNECTION_FAILED = "Failed to connect to the database";
public static final String ERROR_AUTHENTICATION_FAILED = "Authentication failed";
// Private constructor to prevent instantiation
private ApplicationConstants() {
throw new AssertionError("Constants class should not be instantiated");
}
}
In this example, ApplicationConstants
provides various constants that can be used throughout the application. These constants cannot be changed once the class is loaded.
🔐 Final Methods in Java
A final method cannot be overridden by subclasses. This is useful when you want to ensure that a method's behavior remains the same in all subclasses.
Basic Example of Final Variables in Java:
public class Parent {
// Regular method that can be overridden
public void regularMethod() {
System.out.println("Parent's regular method");
}
// Final method that cannot be overridden
public final void finalMethod() {
System.out.println("Parent's final method");
}
}
public class Child extends Parent {
// Overriding the regular method is allowed
@Override
public void regularMethod() {
System.out.println("Child's implementation of regular method");
}
// Uncommenting this would cause a compilation error
/*
@Override
public void finalMethod() {
System.out.println("Child's implementation of final method");
}
*/
}
public class FinalMethodDemo {
public static void main(String[] args) {
Parent parent = new Parent();
Child child = new Child();
System.out.println("Calling methods on parent:");
parent.regularMethod();
parent.finalMethod();
System.out.println("\nCalling methods on child:");
child.regularMethod();
child.finalMethod();
}
}
Output:
Calling methods on parent:
Parent's regular method
Parent's final method
Calling methods on child:
Child's implementation of regular method
Parent's final method
In this example, the Child
class can override the regularMethod
of the Parent
class, but it cannot override the finalMethod
because it is declared as final.
Common Use Cases for Final Methods:
- Security: Preventing subclasses from changing critical behavior
- Performance: Allowing the compiler to optimize method calls (though modern JVMs make this less relevant)
- Design Intent: Clearly indicating that a method's behavior should not be altered
- Framework Design: Ensuring that certain methods in a framework behave consistently
Example with Security:
Click to expand
public class BankAccount {
private String accountNumber;
private double balance;
private final String ownerName;
public BankAccount(String accountNumber, String ownerName, double initialBalance) {
this.accountNumber = accountNumber;
this.ownerName = ownerName;
this.balance = initialBalance;
}
// Regular methods that can be overridden
public void deposit(double amount) {
if (amount > 0) {
balance += amount;
System.out.println("Deposited: $" + amount);
System.out.println("New balance: $" + balance);
}
}
public void withdraw(double amount) {
if (amount > 0 && amount <= balance) {
balance -= amount;
System.out.println("Withdrawn: $" + amount);
System.out.println("New balance: $" + balance);
} else {
System.out.println("Insufficient funds or invalid amount");
}
}
// Final method for security - cannot be overridden
public final void transferFunds(BankAccount destination, double amount) {
if (amount > 0 && amount <= balance) {
// Withdraw from this account
balance -= amount;
// Deposit to destination account
destination.balance += amount;
System.out.println("Transferred: $" + amount + " to account " + destination.accountNumber);
System.out.println("New balance: $" + balance);
} else {
System.out.println("Insufficient funds or invalid amount for transfer");
}
}
// Getters
public String getAccountNumber() {
return accountNumber;
}
public double getBalance() {
return balance;
}
public String getOwnerName() {
return ownerName;
}
}
public class MaliciousBankAccount extends BankAccount {
public MaliciousBankAccount(String accountNumber, String ownerName, double initialBalance) {
super(accountNumber, ownerName, initialBalance);
}
// We can override regular methods
@Override
public void withdraw(double amount) {
// Malicious implementation that doesn't actually withdraw money
System.out.println("Pretending to withdraw: $" + amount);
System.out.println("But not actually changing the balance!");
}
// We cannot override the final transferFunds method
/*
@Override
public void transferFunds(BankAccount destination, double amount) {
// Malicious implementation that transfers twice the amount
super.transferFunds(destination, amount * 2);
}
*/
}
public class BankDemo {
public static void main(String[] args) {
BankAccount account1 = new BankAccount("123456", "John Doe", 1000);
BankAccount account2 = new BankAccount("789012", "Jane Smith", 500);
MaliciousBankAccount maliciousAccount = new MaliciousBankAccount("666666", "Evil Hacker", 100);
System.out.println("Regular account operations:");
account1.deposit(200);
account1.withdraw(50);
account1.transferFunds(account2, 100);
System.out.println("\nMalicious account operations:");
maliciousAccount.deposit(200);
maliciousAccount.withdraw(50); // This uses the overridden method
maliciousAccount.transferFunds(account1, 100); // This uses the final method from the parent
System.out.println("\nFinal balances:");
System.out.println("Account 1: $" + account1.getBalance());
System.out.println("Account 2: $" + account2.getBalance());
System.out.println("Malicious Account: $" + maliciousAccount.getBalance());
}
}
Output:
Regular account operations:
Deposited: $200
New balance: $1200
Withdrawn: $50
New balance: $1150
Transferred: $100 to account 789012
New balance: $1050
Malicious account operations:
Deposited: $200
New balance: $300
Pretending to withdraw: $50
But not actually changing the balance!
Transferred: $100 to account 123456
New balance: $200
Final balances:
Account 1: $1150
Account 2: $600
Malicious Account: $200
In this example, the transferFunds
method in BankAccount
is declared as final to prevent subclasses from overriding it with malicious implementations. The MaliciousBankAccount
class can override the regular withdraw
method with a malicious implementation, but it cannot override the transferFunds
method.
🔐 Final Classes in Java
A final class cannot be extended (subclassed). This is useful when you want to ensure that a class's behavior cannot be altered by inheritance.
Basic Example:
// Final class that cannot be extended
public final class FinalClass {
private int value;
public FinalClass(int value) {
this.value = value;
}
public int getValue() {
return value;
}
public void setValue(int value) {
this.value = value;
}
}
// Uncommenting this would cause a compilation error
/*
public class SubClass extends FinalClass {
public SubClass(int value) {
super(value);
}
// Additional methods or overrides...
}
*/
// Regular class that can be extended
public class RegularClass {
private int value;
public RegularClass(int value) {
this.value = value;
}
public int getValue() {
return value;
}
public void setValue(int value) {
this.value = value;
}
}
// This is allowed
public class SubClass extends RegularClass {
public SubClass(int value) {
super(value);
}
// Additional methods or overrides...
public void additionalMethod() {
System.out.println("Additional method in SubClass");
}
}
public class FinalClassDemo {
public static void main(String[] args) {
FinalClass finalObj = new FinalClass(10);
System.out.println("Final class value: " + finalObj.getValue());
RegularClass regularObj = new RegularClass(20);
System.out.println("Regular class value: " + regularObj.getValue());
SubClass subObj = new SubClass(30);
System.out.println("Subclass value: " + subObj.getValue());
subObj.additionalMethod();
}
}
Output:
Final class value: 10
Regular class value: 20
Subclass value: 30
Additional method in SubClass
In this example, FinalClass
is declared as final and cannot be extended. RegularClass
is not final and can be extended by SubClass
.
Common Use Cases for Final Classes:
- Immutability: Ensuring that a class's behavior cannot be changed
- Security: Preventing malicious subclassing
- Design Intent: Clearly indicating that a class is not designed for extension
- Performance: Allowing the compiler to optimize method calls (though modern JVMs make this less relevant)
Example with Immutability:
// Final immutable class
public final class ImmutablePerson {
private final String name;
private final int age;
private final List<String> hobbies;
public ImmutablePerson(String name, int age, List<String> hobbies) {
this.name = name;
this.age = age;
// Create a defensive copy to ensure immutability
this.hobbies = new ArrayList<>(hobbies);
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
public List<String> getHobbies() {
// Return a defensive copy to ensure immutability
return new ArrayList<>(hobbies);
}
// No setters - objects are immutable
@Override
public String toString() {
return "ImmutablePerson{name='" + name + "', age=" + age + ", hobbies=" + hobbies + "}";
}
}
public class ImmutabilityDemo {
public static void main(String[] args) {
List<String> hobbiesList = new ArrayList<>();
hobbiesList.add("Reading");
hobbiesList.add("Swimming");
ImmutablePerson person = new ImmutablePerson("Alice", 30, hobbiesList);
System.out.println("Original person: " + person);
// Try to modify the original list
hobbiesList.add("Running");
System.out.println("Original list after modification: " + hobbiesList);
System.out.println("Person after original list modification: " + person);
// Try to modify the list returned by getHobbies
List<String> personHobbies = person.getHobbies();
personHobbies.add("Cycling");
System.out.println("Modified returned list: " + personHobbies);
System.out.println("Person after returned list modification: " + person);
// Create a new person with different values
ImmutablePerson person2 = new ImmutablePerson("Bob", 25, Arrays.asList("Gaming", "Cooking"));
System.out.println("Second person: " + person2);
}
}
Output:
Original person: ImmutablePerson{name='Alice', age=30, hobbies=[Reading, Swimming]}
Original list after modification: [Reading, Swimming, Running]
Person after original list modification: ImmutablePerson{name='Alice', age=30, hobbies=[Reading, Swimming]}
Modified returned list: [Reading, Swimming, Cycling]
Person after returned list modification: ImmutablePerson{name='Alice', age=30, hobbies=[Reading, Swimming]}
Second person: ImmutablePerson{name='Bob', age=25, hobbies=[Gaming, Cooking]}
In this example, ImmutablePerson
is a final class with final fields and no setters, making it immutable. The class also creates defensive copies of the mutable List
to ensure that the internal state cannot be modified.
🚫 Common Pitfalls with Java Final Keyword
While the final
keyword is useful, there are some common pitfalls to avoid:
1. False Sense of Immutability in Java
Declaring a reference variable as final only prevents reassignment, not modification of the referenced object.
public class FalseImmutabilityDemo {
public static void main(String[] args) {
// Final reference to a mutable object
final List<String> names = new ArrayList<>();
names.add("Alice");
// We can still modify the list
names.add("Bob");
names.remove("Alice");
System.out.println("Names: " + names);
}
}
Output:
Names: [Bob]
In this example, even though names
is final, we can still modify the list by adding and removing elements.
2. Overuse of Final Classes in Java
Making too many classes final can reduce the flexibility of your code and make it harder to extend or test.
// This might be too restrictive
public final class TooRestrictive {
// Implementation...
}
// Better approach: make the class extendable but methods final where needed
public class BetterApproach {
// Public API methods that should not be overridden
public final void criticalMethod() {
// Implementation...
}
// Methods that can be safely overridden
public void extensibleMethod() {
// Implementation...
}
}
3. Forgetting Defensive Copies in Java Immutable Classes
When creating immutable classes, forgetting to make defensive copies of mutable objects can lead to unexpected behavior.
// Incorrect immutable class
public final class IncorrectImmutablePerson {
private final String name;
private final List<String> hobbies;
public IncorrectImmutablePerson(String name, List<String> hobbies) {
this.name = name;
// No defensive copy - this is a mistake
this.hobbies = hobbies;
}
public String getName() {
return name;
}
public List<String> getHobbies() {
// No defensive copy - this is a mistake
return hobbies;
}
}
// Correct immutable class
public final class CorrectImmutablePerson {
private final String name;
private final List<String> hobbies;
public CorrectImmutablePerson(String name, List<String> hobbies) {
this.name = name;
// Defensive copy
this.hobbies = new ArrayList<>(hobbies);
}
public String getName() {
return name;
}
public List<String> getHobbies() {
// Defensive copy
return new ArrayList<>(hobbies);
}
}
4. Performance Considerations
Creating defensive copies in immutable classes can impact performance if done excessively.
public final class PerformanceConsideration {
private final List<String> items;
public PerformanceConsideration(List<String> items) {
// Defensive copy - necessary for immutability
this.items = new ArrayList<>(items);
}
public List<String> getItems() {
// Defensive copy - necessary for immutability but can impact performance
// if called frequently with large lists
return new ArrayList<>(items);
}
// Alternative approach for better performance
public String getItem(int index) {
if (index < 0 || index >= items.size()) {
throw new IndexOutOfBoundsException("Index: " + index);
}
return items.get(index);
}
public int getItemCount() {
return items.size();
}
}
🌟 Java Final Keyword Best Practices
To use the final
keyword effectively and avoid common pitfalls, follow these best practices:
1. Use Final for Constants
Constants should be declared as static final
.
public class Constants {
public static final double PI = 3.14159265359;
public static final String APP_NAME = "MyApp";
public static final int MAX_CONNECTIONS = 100;
}
2. Use Final for Immutable Objects
When creating immutable objects, make the class final and all fields final.
public final class ImmutablePoint {
private final int x;
private final int y;
public ImmutablePoint(int x, int y) {
this.x = x;
this.y = y;
}
public int getX() {
return x;
}
public int getY() {
return y;
}
// No setters - objects are immutable
// Methods to create new objects based on this one
public ImmutablePoint translate(int dx, int dy) {
return new ImmutablePoint(x + dx, y + dy);
}
@Override
public String toString() {
return "(" + x + ", " + y + ")";
}
}
3. Use Final for Method Parameters
Using final for method parameters can help prevent accidental reassignment.
public class ParameterExample {
public void processData(final String input, final int count) {
// input = "modified"; // Error: Cannot assign a value to final variable
// count = 10; // Error: Cannot assign a value to final variable
// Process the data...
System.out.println("Processing " + count + " items from " + input);
}
}
4. Make Defensive Copies for Mutable Objects
When creating immutable classes, make defensive copies of mutable objects.
public final class ImmutablePerson {
private final String name;
private final Date birthDate;
public ImmutablePerson(String name, Date birthDate) {
this.name = name;
// Defensive copy
this.birthDate = new Date(birthDate.getTime());
}
public String getName() {
return name;
}
public Date getBirthDate() {
// Defensive copy
return new Date(birthDate.getTime());
}
@Override
public String toString() {
return "ImmutablePerson{name='" + name + "', birthDate=" + birthDate + "}";
}
}
5. Consider Using Final for Local Variables
Using final for local variables can help prevent accidental reassignment and make the code more readable.
public class LocalVariableExample {
public void processOrder(Order order) {
final Customer customer = order.getCustomer();
final double totalPrice = calculateTotalPrice(order);
final double tax = totalPrice * 0.1;
final double finalPrice = totalPrice + tax;
System.out.println("Order for " + customer.getName());
System.out.println("Total price: $" + totalPrice);
System.out.println("Tax: $" + tax);
System.out.println("Final price: $" + finalPrice);
}
private double calculateTotalPrice(Order order) {
// Calculate and return the total price
return 100.0; // Placeholder
}
}
🔄 Combining Static and Final in Java
The static
and final
keywords are often used together, especially for constants. Let's explore some common patterns and best practices.
Static Final Constants
The most common use of static final
is for constants. These are values that are the same for all instances of a class and cannot be changed.
public class MathConstants {
public static final double PI = 3.14159265359;
public static final double E = 2.71828182846;
public static final double GOLDEN_RATIO = 1.61803398875;
}
Static Final Reference Variables
When a reference variable is declared as static final
, the reference cannot be changed, but the object it refers to might still be mutable.
public class Configuration {
// Static final reference to a mutable object
public static final List<String> ALLOWED_USERS = new ArrayList<>();
// Static block to initialize the list
static {
ALLOWED_USERS.add("admin");
ALLOWED_USERS.add("user1");
ALLOWED_USERS.add("user2");
}
}
public class ConfigurationDemo {
public static void main(String[] args) {
System.out.println("Allowed users: " + Configuration.ALLOWED_USERS);
// We can modify the list
Configuration.ALLOWED_USERS.add("user3");
System.out.println("Allowed users after modification: " + Configuration.ALLOWED_USERS);
// But we cannot reassign the reference
// Configuration.ALLOWED_USERS = new ArrayList<>(); // Error: Cannot assign a value to final variable
}
}
Output:
Allowed users: [admin, user1, user2]
Allowed users after modification: [admin, user1, user2, user3]
Static Final Immutable Collections
To create truly immutable collections, you can use the Collections.unmodifiableXXX
methods.
public class ImmutableConfiguration {
// Static final reference to an immutable list
public static final List<String> ALLOWED_USERS;
// Static block to initialize the immutable list
static {
List<String> users = new ArrayList<>();
users.add("admin");
users.add("user1");
users.add("user2");
ALLOWED_USERS = Collections.unmodifiableList(users);
}
}
public class ImmutableConfigurationDemo {
public static void main(String[] args) {
System.out.println("Allowed users: " + ImmutableConfiguration.ALLOWED_USERS);
try {
// This will throw an UnsupportedOperationException
ImmutableConfiguration.ALLOWED_USERS.add("user3");
} catch (UnsupportedOperationException e) {
System.out.println("Cannot modify the immutable list: " + e.getMessage());
}
}
}
Output:
Allowed users: [admin, user1, user2]
Cannot modify the immutable list: null
Static Final Methods
Methods can also be declared as static final
, although this is less common because static methods cannot be overridden anyway.
public class UtilityClass {
// This is redundant - static methods cannot be overridden anyway
public static final void utilityMethod() {
System.out.println("Utility method");
}
}
💡 Why Static and Final Matter in Java
Understanding and properly using the static
and final
keywords is crucial for writing efficient, secure, and maintainable Java code. Here's why they matter:
Memory Efficiency
- Static Members: Shared across all instances, reducing memory usage
- Final Variables: Can be optimized by the compiler and JVM
Performance
- Static Methods: Can be optimized by the compiler and JVM
- Final Methods: Can be inlined by the compiler, potentially improving performance
Security
- Final Classes: Prevent malicious subclassing
- Final Methods: Ensure critical behavior cannot be changed
- Final Variables: Ensure values cannot be changed
Design and Maintainability
- Static Utility Methods: Provide functionality without requiring object instantiation
- Static Factory Methods: Provide clear, descriptive ways to create objects
- Final Classes: Clearly indicate that a class is not designed for extension
- Final Methods: Clearly indicate that a method's behavior should not be altered
Concurrency
- Static Variables: Shared across all threads, requiring careful synchronization
- Final Variables: Thread-safe once initialized, reducing the need for synchronization
📝 Java Final Keyword: Key Takeaways
Static Keyword
- Static Variables: Belong to the class, not instances; shared across all instances
- Static Methods: Can be called without creating an instance; cannot access instance variables directly
- Static Blocks: Executed when the class is loaded; used for static initialization
- Static Nested Classes: Can be instantiated without an outer class instance; cannot access outer class instance variables directly
Final Keyword
- Final Variables: Cannot be reassigned after initialization
- Final Methods: Cannot be overridden by subclasses
- Final Classes: Cannot be extended (subclassed)
Best Practices
- Use
static final
for constants - Use
static
for utility methods and factory methods - Use
final
for immutable objects and security-critical methods - Make defensive copies of mutable objects in immutable classes
- Consider thread safety when using static variables
Common Pitfalls
- Memory leaks with static variables
- Thread safety issues with static variables
- False sense of immutability with final reference variables
- Overuse of final classes reducing flexibility
- Forgetting defensive copies in immutable classes
🏋️ Exercises and Mini-Projects
Exercise 1: Utility Class
Create a utility class called StringUtils
with the following static methods:
isEmpty(String str)
: Checks if a string is null or emptycapitalize(String str)
: Capitalizes the first letter of a stringreverse(String str)
: Reverses a stringcountOccurrences(String str, char c)
: Counts occurrences of a character in a string
public final class StringUtils {
// Private constructor to prevent instantiation
private StringUtils() {
throw new AssertionError("Utility class should not be instantiated");
}
/**
* Checks if a string is null or empty.
*
* @param str the string to check
* @return true if the string is null or empty, false otherwise
*/
public static boolean isEmpty(String str) {
return str == null || str.trim().isEmpty();
}
/**
* Capitalizes the first letter of a string.
*
* @param str the string to capitalize
* @return the capitalized string, or the original string if it's empty
*/
public static String capitalize(String str) {
if (isEmpty(str)) {
return str;
}
return Character.toUpperCase(str.charAt(0)) +
(str.length() > 1 ? str.substring(1) : "");
}
/**
* Reverses a string.
*
* @param str the string to reverse
* @return the reversed string, or the original string if it's empty
*/
public static String reverse(String str) {
if (isEmpty(str)) {
return str;
}
return new StringBuilder(str).reverse().toString();
}
/**
* Counts occurrences of a character in a string.
*
* @param str the string to search in
* @param c the character to count
* @return the number of occurrences, or 0 if the string is empty
*/
public static int countOccurrences(String str, char c) {
if (isEmpty(str)) {
return 0;
}
int count = 0;
for (int i = 0; i < str.length(); i++) {
if (str.charAt(i) == c) {
count++;
}
}
return count;
}
}
Exercise 2: Immutable Class
Create an immutable class called ImmutableStudent
with the following requirements:
- Fields for name, ID, and a list of courses
- A constructor that initializes all fields
- Getter methods for all fields
- No setter methods
- Proper defensive copies for mutable objects
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public final class ImmutableStudent {
private final String name;
private final int id;
private final List<String> courses;
public ImmutableStudent(String name, int id, List<String> courses) {
this.name = name;
this.id = id;
// Defensive copy to ensure immutability
this.courses = courses != null ? new ArrayList<>(courses) : new ArrayList<>();
}
public String getName() {
return name;
}
public int getId() {
return id;
}
public List<String> getCourses() {
// Return an unmodifiable view to ensure immutability
return Collections.unmodifiableList(courses);
}
@Override
public String toString() {
return "Student{name='" + name + "', id=" + id + ", courses=" + courses + "}";
}
// Factory method to create a new student with an additional course
public ImmutableStudent addCourse(String course) {
List<String> newCourses = new ArrayList<>(courses);
newCourses.add(course);
return new ImmutableStudent(name, id, newCourses);
}
}
Exercise 3: Singleton Pattern
Implement a thread-safe singleton class called DatabaseConnection
using the static keyword:
public class DatabaseConnection {
// Static instance - only one instance will exist
private static volatile DatabaseConnection instance;
// Private constructor to prevent instantiation
private DatabaseConnection() {
// Simulate database connection setup
System.out.println("Creating database connection...");
}
// Static method to get the singleton instance
public static DatabaseConnection getInstance() {
// Double-checked locking for thread safety
if (instance == null) {
synchronized (DatabaseConnection.class) {
if (instance == null) {
instance = new DatabaseConnection();
}
}
}
return instance;
}
// Instance methods
public void executeQuery(String query) {
System.out.println("Executing query: " + query);
}
public void close() {
System.out.println("Closing database connection...");
}
}
Mini-Project: Configuration Manager
Click to expand to see the Code
Create a configuration manager system that uses static and final keywords appropriately:import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class ConfigurationManager {
// Default configuration values
public static final String DEFAULT_HOST = "localhost";
public static final int DEFAULT_PORT = 8080;
public static final int DEFAULT_TIMEOUT = 30000;
public static final String DEFAULT_LOG_LEVEL = "INFO";
// Thread-safe map for configuration values
private static final Map<String, Object> configValues = new ConcurrentHashMap<>();
// Static initialization block
static {
// Initialize with default values
configValues.put("host", DEFAULT_HOST);
configValues.put("port", DEFAULT_PORT);
configValues.put("timeout", DEFAULT_TIMEOUT);
configValues.put("logLevel", DEFAULT_LOG_LEVEL);
}
// Private constructor to prevent instantiation
private ConfigurationManager() {
throw new AssertionError("Configuration manager should not be instantiated");
}
// Get a configuration value
@SuppressWarnings("unchecked")
public static <T> T getValue(String key, T defaultValue) {
return (T) configValues.getOrDefault(key, defaultValue);
}
// Set a configuration value
public static void setValue(String key, Object value) {
configValues.put(key, value);
}
// Get all configuration values (immutable view)
public static Map<String, Object> getAllValues() {
return Collections.unmodifiableMap(new HashMap<>(configValues));
}
// Reset to default values
public static void resetToDefaults() {
configValues.clear();
configValues.put("host", DEFAULT_HOST);
configValues.put("port", DEFAULT_PORT);
configValues.put("timeout", DEFAULT_TIMEOUT);
configValues.put("logLevel", DEFAULT_LOG_LEVEL);
}
}
// Example usage
public class ConfigurationExample {
public static void main(String[] args) {
// Get default values
String host = ConfigurationManager.getValue("host", ConfigurationManager.DEFAULT_HOST);
int port = ConfigurationManager.getValue("port", ConfigurationManager.DEFAULT_PORT);
System.out.println("Default configuration:");
System.out.println("Host: " + host);
System.out.println("Port: " + port);
// Change some values
ConfigurationManager.setValue("host", "example.com");
ConfigurationManager.setValue("port", 9090);
ConfigurationManager.setValue("maxConnections", 100);
System.out.println("\nUpdated configuration:");
System.out.println("Host: " + ConfigurationManager.getValue("host", ""));
System.out.println("Port: " + ConfigurationManager.getValue("port", 0));
System.out.println("Max Connections: " + ConfigurationManager.getValue("maxConnections", 0));
// Get all values
Map<String, Object> allValues = ConfigurationManager.getAllValues();
System.out.println("\nAll configuration values:");
for (Map.Entry<String, Object> entry : allValues.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue());
}
// Try to modify the returned map (should throw an exception)
try {
allValues.put("newKey", "newValue");
} catch (UnsupportedOperationException e) {
System.out.println("\nCannot modify the configuration map directly: " + e.getClass().getSimpleName());
}
// Reset to defaults
ConfigurationManager.resetToDefaults();
System.out.println("\nAfter reset:");
System.out.println("Host: " + ConfigurationManager.getValue("host", ""));
System.out.println("Port: " + ConfigurationManager.getValue("port", 0));
System.out.println("Max Connections: " + ConfigurationManager.getValue("maxConnections", 0));
}
}
🔍 Quiz
Test your understanding of static and final keywords with this quiz:
- What is the difference between a static variable and an instance variable?
- Can a static method access instance variables directly? Why or why not?
- What happens if you try to override a final method in a subclass?
- When a reference variable is declared as final, can the object it refers to be modified?
- What is the main purpose of making a class final?
- How does the static keyword affect memory usage in a Java application?
- What is a static initialization block and when is it executed?
- How can you create a truly immutable collection in Java?
- What are the thread safety implications of static variables?
- What is the difference between
static final
and justfinal
for a variable?
🎯 Conclusion
The static
and final
keywords are powerful tools in Java that, when used correctly, can improve the efficiency, security, and maintainability of your code. By understanding their purposes and best practices, you can write better Java code and avoid common pitfalls.
Remember these key points:
- Use
static
for class-level members that should be shared across all instances - Use
final
for variables that should not be reassigned, methods that should not be overridden, and classes that should not be extended - Be mindful of thread safety when using static variables
- Make defensive copies when creating immutable classes with mutable objects
- Use
static final
for constants
With these concepts in mind, you'll be well-equipped to use the static
and final
keywords effectively in your Java applications.