• Access specifiers in Java

    Introduction

    In Java, access specifiers (also called access modifiers) define the scope and visibility of classes, methods, variables, and constructors. They are an essential part of object-oriented programming because they enforce encapsulation, prevent misuse of code, and allow developers to design secure and maintainable applications.

    By controlling how different parts of a program interact, access specifiers help you decide who can see what in your code.

    What Are Access Specifiers?

    Access specifiers in Java are keywords that control the visibility of classes and their members. Their primary purpose is to:

    • Enforce encapsulation → Hide implementation details.
    • Provide controlled access → Expose only what is necessary.
    • Improve security → Prevent unauthorized use or modification.
    • Enhance maintainability → Reduce unintended dependencies.

    Types of Access Specifiers in Java

    Java provides four levels of access control:

    Default (Package-Private)

    • No keyword is used.
    • Accessible only within the same package.

    Private

    • Accessible only within the declared class.
    • Cannot be applied to top-level classes.

    Protected

    • Accessible within the same package and by subclasses, even if they are in different packages.
    • Useful in inheritance scenarios.

    Public

    • Typically used for APIs or classes designed for global use.
    • Accessible from anywhere, across all packages and classes.

    1. Default (Package-Private) Access

    If no access modifier is specified, Java applies default (package-private) access.

    class MyClass {
        void display() {
            System.out.println("Default access method");
        }
    }
    
    • When no access modifier is specified, it is considered the default access modifier, which allows access to class members only within the same package.
    • Useful for creating package-level APIs or services.

    2. Private Access

    Private members are strictly confined to their class.

    public class Example {
        private int age;
    
        private void printAge() {
            System.out.println("Age: " + age);
        }
    }
    
    • The private access modifier restricts access to a class, method, or variable within the same class only; it cannot be accessed from any other class.
    • Ideal for sensitive data (e.g., passwords, configurations).
    • Note: Top-level classes cannot be private.

    3. Protected Access

    Protected members are more permissive than private but still controlled.

    package animals;
    
    public class Animal {
        protected void sound() {
            System.out.println("Animal makes a sound");
        }
    }
    
    package pets;
    import animals.Animal;
    
    public class Dog extends Animal {
        public void bark() {
            sound(); // accessible because Dog is a subclass
        }
    }
    
    • The protected access modifier allows access to class members within the same package and also to subclasses in different packages.
    • Commonly used in inheritance for customization.

    4. Public Access

    Public members are universally accessible.

    public class User {
        public void login() {
            System.out.println("User logged in");
        }
    }
    
    • The public access modifier makes a class, method, or variable accessible from any other class, with no restrictions on its visibility.
    • Often used for entry points (e.g., main method), API methods, and utility classes.

    Scope and Usage Table

    SpecifierClassPackageSubclassWorld
    DefaultYesYesYes (same package)No
    PrivateYesNoNoNo
    ProtectedYesYesYes (even in other packages)No
    PublicYesYesYesYes

    Access Modifiers with Classes

    • public Class
    • A public class can be accessed from any other class, in any package.

    ✔️Example:

    // File: Vehicle.java
    public class Vehicle {
        public void display() {
            System.out.println("This is a public class.");
        }
    }
    
    // File: TestVehicle.java (in the same or different package)
    public class TestVehicle {
        public static void main(String[] args) {
            Vehicle v = new Vehicle();
            v.display();  // Accessible everywhere
        }
    }
    • Default Class (Package-Private)
    • If no modifier is specified, the class is package-private and accessible only within the same package.

    ✔️Example :

    // File: Engine.java
    class Engine {
        void showType() {
            System.out.println("This is a default class.");
        }
    }
    
    // File: TestEngine.java (Same Package)
    public class TestEngine {
        public static void main(String[] args) {
            Engine e = new Engine();
            e.showType();  // Accessible because it's in the same package
        }
    }

    👉 Classes cannot be declared private or protected.

    Access Modifiers with Interface

    By default, interface members (methods and variables) are public.

    • Methods are implicitly public abstract.
    • Variables are implicitly public static final.
    public interface Animal {
        // Implicitly public abstract method
        void sound();
    
        // Implicitly public static final variable
        int LEGS = 4;
    }
    
    public class Dog implements Animal {
        @Override
        public void sound() {
            System.out.println("Dog barks.");
        }
    
        public void showLegs() {
            System.out.println("Legs: " + LEGS);
        }
    }
    
    public class TestAnimal {
        public static void main(String[] args) {
            Dog dog = new Dog();
            dog.sound();      // Output: Dog barks.
            dog.showLegs();   // Output: Legs: 4
        }
    }

    👉 Access modifiers cannot restrict interface methods or variables further.

    Access Modifiers in Inheritance

    Inheritance allows a subclass to inherit fields and methods from a superclass. Access modifiers determine what can be inherited.

    ✔️Example of Access Modifier Effect in Inheritance

    public class Parent {
      public int publicVar = 1;
      protected int protectedVar = 2;
      int defaultVar = 3; // Package-private
      private int privateVar = 4;
    
      public void showAll() {
        System.out.println("Public: " + publicVar);
        System.out.println("Protected: " + protectedVar);
        System.out.println("Default: " + defaultVar);
        System.out.println("Private: " + privateVar);
      }
    }
    
    public class Child extends Parent {
      public void display() {
        System.out.println("Access publicVar: " + publicVar); // Accessible
        System.out.println("Access protectedVar: " + protectedVar); // Accessible
        System.out.println("Access defaultVar: " + defaultVar); // Accessible if same package
        // System.out.println("Access privateVar: " + privateVar);     // ❌ Not accessible
      }
    }
    
    public class TestInheritance {
      public static void main(String[] args) {
        Child c = new Child();
        c.display();
      }
    }

    Summary Table – Access Modifiers in Inheritance

    ModifierSame ClassSubclass (Same Package)Subclass (Different Package)Other Classes
    public
    protected
    default
    private

    Best Practices for Access Specifiers

    • Use private by default → Start with the most restrictive access and open up only if required.
    • Encapsulate sensitive data → Keep variables private and expose them via getters/setters if necessary.
    • Use protected carefully → Only for methods intended to be customized by subclasses.
    • Use public sparingly → Overexposing methods leads to tight coupling and harder maintenance.
    • Prefer default access → For internal classes and methods that should stay package-bound.

    Common Mistakes to Avoid

    • Declaring top-level classes as private/protected → Not allowed, only public or default is permitted.
    • Overusing public → Leads to poor encapsulation and fragile code.
    • Forgetting constructor visibility → A class with a private constructor cannot be instantiated from outside (common in Singleton design pattern).
    • Mixing access with unrelated logic → Choose access levels based on design needs, not convenience.

    Conclusion

    Access specifiers are a fundamental concept in Java that every developer must master. By carefully choosing the right modifier—private, default, protected, or public—you can design classes that are secure, extensible, and maintainable.

  • Java Data Types

    1. Introduction

    In Java, every variable has a data type, which defines both the kind of values it can store and the operations that can be performed on it. As a result, data types act as the foundation of Java programming because they ensure type safety while also supporting efficient memory management. Moreover, when you select the correct data type, your program runs more smoothly and consumes less memory. On the other hand, choosing the wrong type may waste resources or even cause errors during execution.

    2. Categories of Data Types in Java

    Java provides two main categories of data types:

    1. Primitive Data Types (built-in, predefined by Java)
    2. Non-Primitive Data Types (objects and references)

    3. Primitive Data Types in Java

    Java has 8 primitive data types. These are the simplest building blocks.

    Data TypeSizeDefault ValueRangeExample Usage
    byte8-bit0-128 to 127byte b = 100;
    short16-bit0-32,768 to 32,767short s = 3000;
    int32-bit0-2,147,483,648 to 2,147,483,647int num = 100000;
    long64-bit0Lhuge rangelong big = 1000000000L;
    float32-bit0.0f7 decimal digits precisionfloat pi = 3.14f;
    double64-bit0.0d15 decimal digits precisiondouble d = 3.14159265359;
    char16-bit‘\u0000’0 to 65,535 (Unicode)char c = ‘A’;
    boolean1 bit (JVM dependent)falsetrue / falseboolean flag = true;

    📝 Key Notes:

    • int works best for most integer calculations
    • When numbers go beyond the int range, switch to long
    • For decimal values, you can choose floator double, though double is usually preferred for higher precision.
    • A boolean is the right choice whenever you need to represent true/false conditions.

    4. Non-Primitive Data Types in Java

    Non-primitive (also called reference types) store memory addresses of objects instead of raw values.

    Examples include:

    • Strings (String name = "Java";)
    • Arrays (int[] numbers = {1,2,3};)
    • Classes (class Person { })
    • Interfaces
    • Objects created from custom classes

    💡 Non-primitive types are created by programmers and are not defined directly by the Java language, except String (which is special).

    5. Type Conversion in Java

    Java allows converting between compatible data types.

    Widening Conversion (Automatic / Implicit)

    Smaller → Larger type conversion happens automatically.
    Example:

    int num = 100;
    double d = num; // automatic conversion
    

    Narrowing Conversion (Explicit / Casting)

    Larger → Smaller type conversion requires explicit casting.

    double d = 9.78;
    int num = (int) d; // manual casting
    

    6. Wrapper Classes

    Every primitive type has a corresponding 👉wrapper class in java.lang package.
    These are used when working with collections or frameworks that require objects instead of primitives.

    PrimitiveWrapper
    byteByte
    shortShort
    intInteger
    longLong
    floatFloat
    doubleDouble
    charCharacter
    booleanBoolean

    Example:

    int num = 10;
    Integer obj = Integer.valueOf(num); // wrapping
    int n = obj; // unwrapping
    

    7. Memory Usage & Performance

    • Primitive types are stored directly in stack memory, fast and efficient.
    • Objects (non-primitives) are stored in heap memory, and variables hold references to them.
    • Choosing the right data type improves performance and reduces memory consumption.

    8. Real-World Examples

    • Banking application: Use long for account numbers, double for balance.
    • Gaming: Use float for character positions, boolean for game status.
    • Text Processing: Use String for player names, char for symbols.

    📝Summary

    • Java provides 8 primitive types and multiple non-primitive types.
    • On the other hand, primitives are fast and memory-efficient, while objects are powerful and flexible.
    • Always choose the appropriate type for efficiency.
    • Wrapper classes allow primitives to be used as objects.

    FAQ Section

    Q1: What are the 8 primitive data types in Java?
    They are byte, short, int, long, float, double, char, boolean.

    Q2: What is the difference between primitive and non-primitive data types in Java?
    Primitives store raw values, while non-primitives store references to objects.

    Q3: Why use double instead of float in Java?
    double is more precise (15 digits) compared to float (7 digits).

    Q4: Is String a primitive type in Java?
    No, String is a non-primitive type, but it’s treated specially in Java.

  • Structure of a Java program

    The structure of a Java program is organized into distinct components that work together to define how the program operates. At its core, every Java program includes a class definition, the main method, and the entry point for execution. In addition, programs often use packages and comments to improve organization and readability.

    1. General Structure of a Java Program

    A simple Java program typically includes:

    1. Package Declaration (optional)
    2. Import Statements (optional)
    3. Class Definition (mandatory)
    4. Main Method (mandatory for standalone execution)
    5. Statements and Expressions

    Example: Hello World Program

    // Step 1: Package Declaration (optional)
    package com.mypackagename;
    
    // Step 2: Import Statements (optional)
    import java.util. * ;
    
    // Step 3: Class Definition
    public class HelloWorld {
    
      // Step 4: Main Method
      public static void main(String[] args) {
    
        // Step 5: Statements
        System.out.println("Hello, World!");
      }
    }

    1. Package Declaration

    • Packages are used to organize classes.
    • Must be the first statement in the program (except comments).
    package com.mypackagename;

    2.Import Statements

    • Used to access classes from other packages.
    • Example:
    • You can also use import java.util.*; to import all classes from a package.
    import java.util.Scanner;

    3. Class Definition

    • A class in Java is a blueprint or template for creating objects.
    • It defines the data (fields or variables) and behavior (methods) of those objects.
    • In fact, every Java program must have at least one class. If declared public, the class can be accessed from other packages.
    • The class body is enclosed in curly braces {} and typically contains variables, constructors, and methods.

    Syntax Example:

    public class MyClass {
        // fields (data members)
        // methods (behavior)
    }

    4. Main Method

    • The main method is the entry point of any standalone Java application.
    • The JVM always starts program execution from this method.
    • It must follow the exact signature:
    public static void main(String[] args) {
        // code to execute
    }
    

    Explanation of keywords:

    • public → makes the method accessible to the JVM.
    • static → allows the method to be called without creating an instance of the class.
    • void → means the method does not return a value.
    • String[]→ is the data type used to represent an array of text values in Java. The square brackets [ ] indicate that it is an array type.
    • args → is the name of the method parameter of type String[]. This allows the main method to accept multiple text inputs (command-line arguments) when the program starts. These arguments are typically used to pass user input or configuration data to the program at runtime.

    Entry Point of Execution

    📌 Frequently asked questions about the Java main() method —👉 Click here to read more

    • When you run a Java program, the JVM looks for the main method inside the specified class.
    • The code inside main() is executed sequentially until the program finishes or terminates early.
    • Inside main, you can create objects, invoke other methods, or perform operations.

    5. Statements

    • Inside the main method, we write instructions.
    • Example:
    System.out.println("Hello, World!");

    6. Other Elements in Java Programs

    Comments:

    • Improve readability with // for single-line or /* ... */ for multi-line comments.
    // Single-line comment
    /* Multi-line 
       comment */
    

    Variables and Data Types

    • Store data values.
    int number = 10;
    

    Methods

    • Functions inside a class.
    public void greet() {
        System.out.println("Welcome to Java!");
    }
    

    Objects

    • Instances of a class used to call methods.
    HelloWorld obj = new HelloWorld();
    obj.greet();

    ✅ Example: A Simple Java Program

    package myapp;
    
    import java.util.Scanner;
    
    public class StructureExample {
        
        // method
        public void greet(String name) {
            System.out.println("Hello, " + name + "!");
        }
    
        // main method
        public static void main(String[] args) {
            Scanner sc = new Scanner(System.in);
            System.out.print("Enter your name: ");
            String name = sc.nextLine();
    
            // creating object
            StructureExample obj = new StructureExample();
            obj.greet(name);
        }
    }
    

    Explanation:

    • public class StructureExample : Defines a class named StructureExample. The file name must be HelloWorld.java.
    • public static void main(String[] args): This is the entry point method where the program begins execution.
    • System.out.println("Enter your name: ");: This statement prints the text “Hello, World!” to the console.

    📚 Steps to Compile and Run a Java Program

    Step 1: Save the Program

    Save your code to a file named StructureExample.java. In Java, the file name must always match the public class name.

    Step 2: Open Terminal or Command Prompt

    Navigate to the directory where the file is saved.

    Step 3: Compile the Program

    Run the following command to compile the Java file:

    javac StructureExample.java

    This will create a StructureExample.class file containing the bytecode.

    Step 4: Run the Compiled Bytecode

    Now, run the program with:

    java StructureExample

    Output:

    Enter your name: Ashish
    Hello, Ashish!

    📝 Summary

    In summary, the basic structure of a Java program includes a class definition to encapsulate data and behavior, and a main method that acts as the program’s entry point. Moreover, packages, imports, and comments improve maintainability and organization. Understanding this structure is essential for every beginner learning Java programming.

    FAQ Section:

    Q1: What is the main method in Java?
    It is the entry point where execution begins: public static void main(String[] args).

    Q2: Can a Java program run without a main method?
    No, a standalone program requires a main method.

    Q3: Why is class mandatory in Java?
    Because Java is object-oriented; everything must reside inside a class.

    Q4: Can we write multiple classes in one file?
    Yes, but only one public class is allowed, and the filename must match it.

  • Memory Management

    📚Common Causes of OutOfMemoryError(OOM)in JVM

    When the JVM runs out of memory and cannot allocate more objects or native resources, it throws an OutOfMemoryError. This error can occur in several scenarios. Therefore, understanding the root causes is important for diagnosing and fixing memory problems effectively.

    1. Java Heap Space Exhaustion

    • This occurs when the heap memory is full and no more objects can be allocated.
    • As a result, the Garbage Collector cannot free enough space to handle new allocations.
    • Common causes include:
      • Memory leaks, where objects are unintentionally held in memory.
      • Sudden spikes in object creation, which exceed the available heap.
      • Insufficient maximum heap size (-Xmx set too low).

    2. GC Overhead Limit Exceeded

    • This happens when the JVM spends too much time (>98%) on garbage collection but recovers very little memory (<2%).
    • In other words, the heap is almost full, and GC cannot reclaim enough space to keep the application running efficiently.

    3. Metaspace Exhaustion (Java 8 and later)

    • Metaspace stores class metadata, such as class definitions and methods.
    • Unlike the old PermGen, Metaspace grows dynamically. However, it can still run out of space if not managed properly.
    • Causes include:
      • Loading too many classes dynamically.
      • ClassLoader leaks, where classes are never unloaded.
    • In such cases, you may see the error: java.lang.OutOfMemoryError: Metaspace.

    4. Native Memory Exhaustion

    • The JVM also relies on native (off-heap) memory for several purposes, including:
      • Thread stacks
      • Direct byte buffers (DirectByteBuffer)
      • Code cache (JIT compiled code)
      • JNI/native library allocations
    • Causes: If these resources are overused, the JVM can fail. For example:-
      • Creating too many threads can cause unable to create new native thread.
      • Memory leaks in direct buffer allocations may also lead to OOM errors

    5. Array Size Limit Exceeded

    • The JVM has a platform-dependent maximum array size (close to Integer.MAX_VALUE, ~2 billion).
    • Consequently, requesting an array larger than this limit results in:
      • java.lang.OutOfMemoryError: Requested array size exceeds VM limit.

    6. Kernel or OS Memory Limits

    • Even if the JVM is configured with enough memory, the operating system or container may impose stricter limits.
    • For example:
      • Running in Docker/Kubernetes with strict memory quotas can trigger OOM errors.
      • OS-level restrictions on processes or threads may also prevent new allocations.

    📝Summary of Key Causes

    In summary, OutOfMemoryError in Java occurs when the JVM cannot allocate the required memory due to one of several reasons: heap space exhaustion, excessive GC activity, Metaspace leaks, native memory shortages, oversized arrays, or memory limits imposed by the OS or container. Therefore, by identifying the correct cause, developers can apply targeted fixes to resolve the issue

    📚Java Heap Sizing Best Practices with -Xms and -Xmx

    The JVM provides the flags -Xms (initial heap size) and -Xmx (maximum heap size) to control heap memory allocation. Proper sizing is critical because it directly impacts application performance, garbage collection behavior, and system stability.

    # JVM Heap Sizing Flags
    -Xms<size>   # Initial heap size (e.g., -Xms2G)
    -Xmx<size>   # Maximum heap size (e.g., -Xmx8G)
    

    1. Set Initial and Maximum Heap Size Appropriately

    • Use -Xms to define the initial heap size and -Xmx to set the maximum heap size.
    • In many cases, it is recommended to set both values to the same size (for example, -Xms4G -Xmx4G).
    • By doing so, you avoid heap resizing at runtime, which can otherwise introduce performance overhead.

    2. Size Heap Based on Application Needs and Physical Memory

    • The heap should be large enough to hold the application’s live data footprint, which reduces garbage collection frequency.
    • However, it should not be so large that it results in excessive GC pause times.
    • In addition, always ensure that the heap fits comfortably within the available physical memory to prevent OS-level swapping, which slows down performance significantly.

    3. Avoid Setting Heap Too Small

    • A heap that is too small leads to frequent full GCs and may trigger OutOfMemoryError exceptions.
    • Therefore, always monitor GC logs and heap usage metrics to make informed adjustments.

    4. Avoid Oversized Heap Without Proper GC Tuning

    • A very large heap can reduce GC frequency; however, it also increases pause times during collection.
    • Consequently, if you need very large heaps, you should use advanced collectors such as G1GC or ZGC.
    • Moreover, tune GC flags according to your application’s workload to balance throughput and latency.

    5. Rely on Profiling and Monitoring

    • Rather than guessing heap sizes, use profiling and monitoring tools (e.g., JVisualVM, JFR, or GC logs).
    • These tools provide insights into heap usage patterns, GC frequency, and pause durations.
    • As a result, you can make data-driven decisions when setting -Xms and -Xmx.

    6. Consider Platform Limitations

    • The maximum heap size depends on the underlying platform.
    • For example, 32-bit JVMs are limited in addressable memory compared to 64-bit JVMs.
    • Therefore, always check system and container limits before assigning a very large heap.

    📝 Summary

    In summary, optimal Java heap sizing with -Xms and -Xmx depends on balancing application needs, garbage collection performance, and system resources. Start by setting both flags to the same size, monitor heap behavior closely, and adjust based on profiling data rather than arbitrary values