Declare and instantiate Java objects including nested class objects, and explain the object life-cycle including creation, reassigning references, and garbage collection.
Create classes and records, and define and use instance and static fields and methods, constructors, and instance and static initializers.
Implement overloading, including var-arg methods.
java.lang.Object
As the name implies, object-oriented programming (OOP) is a programming paradigm centered around the concept of objects. Rather than structure programs around procedures and functions (like procedural programming), OOP organizes code into objects, which represent real-world entities containing data (attributes) and behaviors (methods). This approach offers several advantages:
Java is an OOP language, so its basic building blocks are objects and classes.
Objects are distinct instances in code that contain data and behaviors. Classes, on the other hand, are blueprints or templates that define the data and behaviors common to all objects of that class.
To better understand these concepts, think of cookies made from a cookie cutter. The cookie cutter defines the shape and size of the cookies, just as classes define what attributes and methods the object instances will have. Each cookie can be unique, with different chocolate chip placements, just as objects contain distinct data values.
For example, we can define a Cookie
class that specifies the attributes of cookies, such as flavor, shape, topping, etc. You can also define methods, which are functions that operate on the data. Methods allow objects to perform actions. Our Cookie
objects could have an eat()
method:
public class Cookie {
// Attributes
String flavor;
int size;
// Behavior (Method)
public void eat() {
System.out.println("That was yummy!");
}
}
public class Cookie
defines a new Cookie
class.public
makes this class accessible from other classes.String flavor;
declares a new String attribute called flavor
.int size;
declares an integer size
attribute.public void eat()
defines a public eat
method that does not return a value (void
).{ }
brackets.System.out.println();
prints text to the standard output (usually the console or terminal window).And we can instantiate cookie objects from the Cookie
class:
Cookie chocoChip = new Cookie();
chocoChip.flavor = "Chocolate Chip";
chocoChip.size = 2;
Cookie oatmealRaisin = new Cookie();
oatmealRaisin.flavor = "Oatmeal Raisin";
oatmealRaisin.size = 1;
Cookie chocoChip = new Cookie();
instantiates a new Cookie
object called chocoChip
.Cookie
and the default constructor new Cookie()
.chocoChip.flavor = "Chocolate Chip";
sets the flavor attribute of chocoChip
.chocoChip.size = 2;
sets the size attribute to 2
.oatmealRaisin
, creating another unique cookie object.The objects chocoChip
and oatmealRaisin
are both cookies with the same methods defined by the Cookie
class. However, they contain different data values for attributes like flavor and size.
A common misconception is that objects and classes are the same. However, while objects and classes are related, they serve distinct purposes:
The class acts as the mold, while objects are the cookies produced.
Once you understand objects and classes, grasping the higher-level principles of OOP, like inheritance, encapsulation, and polymorphism, becomes easier:
Inheritance enables code reuse and the creation of class hierarchies. It’s like having a basic cookie recipe that serves as a template for many types of cookies. This basic recipe (the parent class) includes common ingredients and methods (attributes and behaviors) that all cookies share. Specialized recipes (subclasses) for different types of cookies, like chocolate chip or oatmeal raisin, inherit common elements but also introduce unique ingredients or steps.
Encapsulation involves bundling data attributes and behaviors into class definitions. It’s like wrapping up your cookie dough and recipe instructions into a neat package. Each type of cookie, like chocolate chip or oatmeal raisin, has its own box containing everything needed to make it: ingredients (data) and steps (methods). This package ensures that all the secrets to baking the perfect cookie are held tightly together, accessible only through a specific opening in the box.
Polymorphism enables customizing inherited parent behaviors in subclasses, like overriding the parent eat()
method inside ChocolateChip
to print "Mmm chocolate chip!"
.
Bringing this full circle, we can model real-world cookie hierarchies through:
eat()
per subclass.Together, these core OOP concepts enable flexible, modular cookie class design. We’ll review these concepts in more detail in the next chapter. First, let’s talk about the life-cycle of an object.
Understanding the different stages of an object’s life-cycle is essential in Java’s object-oriented programming. This includes the creation of objects, how reference variables access them, and how unused objects are managed by Java’s garbage collector.
Here’s a diagram that illustrates the typical life-cycle of a Java object, from creation to garbage collection:
┌────────────────────┐
│ Object Creation │
│ (new keyword) │
└────────┬───────────┘
│
▼
┌────────────────────┐
│ Initialization │
│ (Constructor) │
└────────┬───────────┘
│
▼
┌───────────────────┐
│ Object Use │
│ (Active Lifetime) │
└────────┬──────────┘
│
▼
┌────────────────────┐
│ Unreachable │
│(No more references)│
└────────┬───────────┘
│
▼
┌────────────────────┐
│ Garbage Collect │
│ (finalize) │
└────────────────────┘
But to illustrate the life stages of a Java object, let’s use the analogy of a library book. When a new book arrives at the library, it is similar to constructing a new object using the new
keyword. For example:
Book javaBook = new Book("The Java Book");
Let’s break down what happens in that single line step-by-step:
Book javaBook;
This declares a variable called javaBook
of type Book
. At this point, no Book
object exists yet; we have just created a reference variable that can point to a Book
object.
= new Book("The Java Book");
The new
keyword instantiates or constructs a new Book
object. This allocates memory on the heap for the object, passes the string argument to the Book
constructor to initialize its state, and returns a reference to the newly created object.
=
operator assigns the reference of the new Book
object to the javaBook
variable.So, javaBook
now contains a reference pointing to the new Book
instance in memory:
javaBook --> [New Book object]
Here, javaBook
is the reference variable pointing to the newly created Book
instance on the Java heap.
Like library books being checked out by different people, object references in Java can be reassigned. For example:
Book refBook = javaBook; // Assign second reference
javaBook = null; // Remove original reference
Let’s review this step by step:
Book refBook = javaBook;
This creates a new reference variable refBook
and assigns it the value of javaBook
. Both javaBook
and refBook
now point to the same Book
object.
javaBook --> [Book object]
refBook --> [Book object]
javaBook = null;
This sets javaBook
to null
, meaning it no longer refers to any object.
javaBook --> null
refBook --> [Book object]
Only refBook
now points to the Book
object. The object does not qualify for garbage collection because refBook
still references it.
Books no longer borrowed are eventually removed from a library’s catalog. Similarly, in Java, objects with no references are cleaned up by the garbage collector:
refBook = null; // Unreferenced object eligible for garbage collection
When all references to an object are gone, it becomes eligible for garbage collection.
The garbage collection process can be summarized as follows:
Identifying Unused Objects: The garbage collector (GC) periodically scans the heap to find objects no longer referenced by any part of the application.
Reclaiming Memory: Unreferenced objects, which cannot be accessed anymore, are considered garbage. The GC frees the memory occupied by these objects, returning it to the pool of available memory on the heap.
Automatic Management: Garbage collection happens automatically in the background, without explicit program triggering, ensuring that memory management is handled efficiently.
In languages like C, memory must be managed manually by allocating and freeing memory. Java automates this process with garbage collection, increasing programmer productivity and reducing the risk of memory leaks and other related issues.
Now, let’s discuss some concepts we’ll use to declare a class and other elements.
In Java, a keyword is a reserved word that has a predefined meaning in the language. Keywords define the structure and syntax of Java programs. They cannot be used as identifiers (names for variables, methods, classes, etc.) because they are reserved for specific purposes.
Java includes a set of keywords fundamental to the language. Some commonly used keywords include:
class
: Used to declare a class.public
, private
, protected
: Access modifiers that determine the visibility and accessibility of classes, methods, and variables.static
: Indicates that a member belongs to the class itself rather than instances of the class.void
: Specifies that a method does not return a value.if
, else
, switch
, case
: Used for conditional statements.for
, while
, do
: Used for looping and iteration.return
: Used to return a value from a method.new
: Used to create new instances of a class.try
, catch
, finally
: Used for exception handling.import
: Used to import classes or packages.Always keep in mind that each keyword has a specific purpose and is used to define the structure and behavior of Java programs.
Also, it’s important to note that keywords are case-sensitive in Java. For example, class
is a keyword, but Class
is not. Additionally, you cannot use keywords as identifiers, such as variable or method names, because they are reserved by the language.
Here’s an example demonstrating the usage of some keywords:
public class MyClass {
private static int myVariable;
public static void myMethod() {
if (myVariable > 0) {
System.out.println("Positive");
} else {
System.out.println("Negative");
}
}
}
In this example, public
, class
, private
, static
, int
, void
, if
, and else
are all keywords used to define the structure and behavior of the MyClass
class.
We’ll review these and other keywords in the upcoming sections and chapters.
Comments are annotations in the code that are ignored by the compiler. They can be used to:
Java supports three types of comments:
Single-line comments start with two forward slashes (//
). Anything following //
on the same line is ignored by the Java compiler:
// This is a single-line comment
int variable = 1; // This is another single-line comment
Multi-line comments, also known as block comments, start with /*
and end with */
. Everything between /*
and */
is considered a comment, regardless of how many lines it spans:
/* This is a multi-line comment
and it can span multiple lines. */
int variable = 1;
Documentation comments, or javadoc comments, are designed to document the Java code. They start with /**
and end with */
. These comments can be extracted to a HTML document using the Javadoc tool. Documentation comments are mostly used before definitions of classes, interfaces, methods, and fields:
/**
* This is a documentation comment.
* It can be used to describe classes, interfaces, methods, and fields.
*/
public class MyClass {
/**
* This method adds up two int values.
*
* @param a First value
* @param b Second value
* @return The sum of a and b
*/
public int add(int a, int b) {
return a + b;
}
}
A package organizes related classes, interfaces, and sub-packages into a single unit.
For example, imagine you own a grocery store that sells many types of products. To keep things organized and easy to find, you decide to group similar products together in different sections or aisles of the store.
In this analogy:
Just like how you group related products together in the same section of the store, you group related classes and interfaces together in the same package in Java.
For example, in your grocery store, you might have:
Similarly, in your Java project, you can have:
com.example.products
package for classes related to product management, such as Product
, Inventory
, and Category
.com.example.orders
package for classes related to order processing, such as Order
, ShoppingCart
, and Payment
.com.example.auth
package for classes related to user authentication, such as User
, Login
, and Permission
.Here’s a visual representation of these packages and classes:
┌─────────────────────────────────────────────────────────────┐
│ com.example │
│ ┌─────────────────────────┐ ┌─────────────────────────┐ │
│ │ products │ │ orders │ │
│ │ ┌─────────────────┐ │ │ ┌─────────────────┐ │ │
│ │ │ Product.java │ │ │ │ Order.java │ │ │
│ │ └─────────────────┘ │ │ └─────────────────┘ │ │
│ │ ┌─────────────────┐ │ │ ┌─────────────────┐ │ │
│ │ │ Inventory.java │ │ │ │ShoppingCart.java│ │ │
│ │ └─────────────────┘ │ │ └─────────────────┘ │ │
│ │ ┌─────────────────┐ │ │ ┌─────────────────┐ │ │
│ │ │ Category.java │ │ │ │ Payment.java │ │ │
│ │ └─────────────────┘ │ │ └─────────────────┘ │ │
│ └─────────────────────────┘ └─────────────────────────┘ │
│ │
│ ┌─────────────────────────┐ │
│ │ auth │ │
│ │ ┌─────────────────┐ │ │
│ │ │ User.java │ │ │
│ │ └─────────────────┘ │ │
│ │ ┌─────────────────┐ │ │
│ │ │ Login.java │ │ │
│ │ └─────────────────┘ │ │
│ │ ┌─────────────────┐ │ │
│ │ │ Permission.java │ │ │
│ │ └─────────────────┘ │ │
│ └─────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
By organizing your classes into packages, you create a logical structure that makes it easier to locate and manage related code elements, just like how organizing products into sections makes it easier for customers to find what they need in the grocery store.
To create a package, use the package
keyword followed by the package name at the top of your Java source file. For example:
package com.example.mypackage;
The package name should be in lowercase and follow the reverse domain name convention to ensure uniqueness.
The package declaration must be the first statement in the source file, before any import statements or class declarations. The following will not compile:
import java.util.ArrayList; // Import statement before the package declaration
package mypackage; // Package declaration not at the beginning
public class MyClass {
public static void main(String[] args) {
System.out.println("This will not compile.");
}
}
import
statements are used to bring classes or interfaces from other packages into the current namespace. Instead of using the fully qualified name each time you refer to a class from another package, you can use an import
statement to refer to the class by its name. For example:
import java.util.ArrayList;
// ...
ArrayList list = new ArrayList();
If you choose not to use an import
statement for a class from another package, you would have to use the class’s fully qualified name every time you reference it in your code. Remember, the fully qualified name includes both the package name and the class name.
For example, if you do not import the ArrayList
class from the java.util
package, you would have to use java.util.ArrayList
every time you want to create or use an ArrayList
object in your code:
// No import statement for java.util.ArrayList
// ...
java.util.ArrayList list = new java.util.ArrayList();
There are a couple of exceptions or special cases to the rule regarding the use of fully qualified names and import statements:
Classes in the java.lang
Package: Classes and interfaces in the java.lang
package do not need to be imported explicitly, as they are automatically available. For example, you don’t need to import classes like String
, Math
, System
, or wrapper classes like Integer
, Double
, etc.
Same Package: Classes and interfaces that are in the same package as the class you’re writing do not require an import statement. Java automatically looks in the current package for other classes and interfaces if it doesn’t find the referenced class or interface in the imported packages.
Fully Qualified Name Collision: When two classes have the same name but are in different packages, and you need to use both in the same file, you cannot import both directly because of the name collision. In such cases, at least one (and possibly both) must be referred to by their fully qualified names to avoid ambiguity.
Here’s an example to illustrate this last point:
import java.sql.Date;
public class Example {
public static void main(String[] args) {
Date sqlDate = new Date(System.currentTimeMillis());
java.util.Date utilDate = new java.util.Date();
}
}
In this example, Date
from java.sql
is imported, so it can be referred to by its simple name. However, since we also want to use Date
from java.util
, we must refer to it by its fully qualified name to distinguish it from java.sql.Date
.
You can also use a wildcard (*
`) to import all the classes from a package. For example:
import java.util.*;
However, it’s generally recommended to import specific classes rather than using wildcards because they can make the code less readable, lead to naming conflicts if multiple packages have classes with the same name, and add redundancy, such as including a class twice.
Although the compiler allows redundant imports, they can clutter your code and reduce readability.
For example, assuming we have two classes, MyClass
and HelperClass
, in the same package, mypackage
:
// File: HelperClass.java
package mypackage;
public class HelperClass {
public static void doSomething() {
System.out.println("Doing something...");
}
}
The following class illustrates redundant imports:
package mypackage;
import mypackage.HelperClass; // Redundant import because HelperClass is in the same package
import java.util.List; // Redundant import because it's not used in the class
public class MyClass {
public static void main(String[] args) {
HelperClass.doSomething();
}
}
In this example:
import mypackage.HelperClass;
is redundant because HelperClass
is already in the same package as MyClass
. Remember, classes in the same package are automatically available to each other without the need for import statements.import java.util.List;
is also redundant because the List
interface is not used anywhere in MyClass
.Removing these redundant imports would make the code cleaner without affecting its functionality.
Packages provide a level of access control, similar to how certain sections of the store might be restricted to authorized personnel only. You can use access modifiers (public
, protected
, default, private
) to control the visibility and accessibility of classes and members within and across packages.
For example, let’s say you have a package named com.example.internals
that contains classes and methods intended for internal use only within that package:
package com.example.internals;
class InternalClass {
void internalMethod() {
// Internal implementation
}
}
Now, consider another package com.example.api
:
package com.example.api;
import com.example.internals.InternalClass;
public class APIClass {
public void someMethod() {
InternalClass obj = new InternalClass(); // Not accessible
obj.internalMethod(); // Not accessible
}
}
In this example, the InternalClass
and its methods have default (package-private) access. They are accessible within the com.example.internals
package but not from other packages. The APIClass
in the com.example.api
package cannot access the InternalClass
or its methods directly.
Let’s review in more detail the available access modifiers.
Access modifiers are keywords used in classes, methods, or variable declarations to control the visibility of that member from other parts of the program. There are four main types of access modifiers in Java:
public
: The public
access modifier specifies that the member is accessible from any other class in the Java application, regardless of the package it belongs to. Using the public
modifier means there are no restrictions on accessing the member.
protected
: The protected
access modifier allows the member to be accessed within its own package and also by subclasses of its class in other packages. This is less restrictive than package-private but more restrictive than public
.
default
(also known as package-private): If no access modifier is specified, the member has package-private access by default. This means the member is accessible only within its own package and is not visible to classes outside the package. It’s important to note that there is no explicit default
keyword in Java; you simply omit the access modifier.
private
: The private
access modifier specifies that the member is accessible only within the class it is declared in. It is the most restrictive access level and is used to ensure that the member cannot be accessed from outside its own class, not even by subclasses.
Each of these access modifiers serves a specific purpose in the context of object-oriented design and encapsulation. They allow you to structure your code in a way that protects sensitive data and implementation details while exposing necessary functionality to other parts of your application.
Here’s a diagram to understand the scope of each access modifier more easily:
┌─────────────────────────────────────────────────────────────┐
│ public │
│ ┌─────────────────────────────────────────────────┐ │
│ │ protected │ │
│ │ ┌─────────────────────────────────────┐ │ │
│ │ │ default (package-private) │ │ │
│ │ │ ┌─────────────────────────┐ │ │ │
│ │ │ │ private │ │ │ │
│ │ │ └─────────────────────────┘ │ │ │
│ │ └─────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
Access Levels (from most restrictive to least restrictive):
private : Same class only
default : Same package
protected : Same package + subclasses in other packages
public : Accessible from anywhere
In the next sections, we’ll explain access modifiers in the context of classes, fields, and methods. But first, let’s review how to properly declare a class.
A class in Java acts as a blueprint for objects, encapsulating both data and behavior.
The syntax to declare a class follows this format:
[accessModifier] class ClassName [extends Superclass] [implements Interface1, Interface2, ...] {
// class body
}
For example, a class declaration might look like this:
public class MyClass extends MySuperClass implements MyInterface {
private int myField;
public MyClass() {
// Constructor body
}
public void myMethod() {
// Method body
}
}
First, you can optionally specify an access modifier to determine the visibility and accessibility of the class to other parts of a Java application:
public
: The class is accessible from any other class across different packages.Following the optional access modifier, you have to use the class
keyword, followed by the name of the class.
A class name or class identifier should follow the following rules:
Unicode characters: Java allows the use of Unicode characters in identifiers, which means you can use letters from non-Latin alphabets as well. However, this is not commonly used and can lead to code that is difficult to read and maintain.
Alphabetic characters, digits, underscores (_), and dollar signs ($): These are the most common characters used in identifiers. Any combination of these characters is allowed, but class names must not begin with a digit.
No special characters: Other than underscores and dollar signs, special characters such as @
, %
, !
, ?
, #
, &
, *
, ^
, ~
, _
, -
, +
, =
, {
, }
, [
, ]
, |
, ,
, ;
, <
, >
, /
, \
, or '
, are not allowed in class identifiers.
Class names should not contain spaces. This would make the code invalid and lead to compilation errors.
Cannot be a Java reserved word: Identifiers cannot use any of Java’s reserved words (like int
, if
, for
, etc.). Reserved words have specific meanings in Java and cannot be used for class names, variable names, or any other identifiers.
Case Sensitivity: Java is case-sensitive, meaning identifiers like MyClass
, myclass
, and MYCLASS
will be considered different.
Length: There is no length limit for class names in Java.
These rules ensure that class name are syntactically correct and avoid conflicts with Java’s built-in language features. It’s also good practice to follow Java naming conventions on top of these rules, like starting class names with a capital letter and using camel case for multi-word names (like using MyClass
instead of myclass
or MY_CLASS
). But again, this is just a convention, not a rule.
After the class name, you can optionally extend a superclass using the extends
keyword, followed by the name of the superclass. Java supports single inheritance, meaning a class can only extend one superclass.
However, you can implement one or more interfaces using the implements
keyword, followed by a comma-separated list of interface names:
public class MyClass implements MyInterface1, MyInterface2, MyInterface3 {
// ...
}
Finally, you define the class body within a pair of curly braces {}
. The class body contains the members of the class, including fields, methods, constructors, and nested classes.
This way, in the next example:
public class MyClass extends MySuperClass implements MyInterface {
/* Class body begins */
// Fields
private int myField;
// Constructor
public MyClass() {
// Constructor body
}
// Methods
public void myMethod() {
// Method body
}
/* Class body ends */
}
public
is the access modifier, indicating that the class is accessible from anywhere.class
is the keyword used to declare a class.MyClass
is the name of the class.extends MySuperClass
specifies that MyClass
inherits from the MySuperClass
superclass.implements MyInterface
indicates that MyClass
implements the MyInterface
interface.private
field myField
, a public
constructor MyClass()
, and a public
method myMethod()
.Now, before reviewing how to declare fields and methods in more detail, let’s talk about static and instance members.
Classes can have two types of members: static members and instance members. Let’s use the analogy of a TV model to better understand these types of members.
Imagine different TV sets of the same model in different homes. Each TV set represents an instance (object) of the Television
class. The TV model itself represents the class.
Instance members, such as instance variables and instance methods, belong to each individual TV set (object):
changeChannel()
or adjustVolume()
, are actions that each TV set can perform independently.Static members, such as static variables and static methods, belong to the TV model (class) itself:
getManufacturerInfo()
or getModelNumber()
, are actions that belong to the TV model and can be accessed without creating an instance of the Television
class.Here’s the Television
class:
public class Television {
// Instance fields
private int currentChannel;
private int volume;
private boolean isOn;
// Static field
private static String manufacturerLogo = "MyBrand";
// Instance method
public void changeChannel(int channel) {
this.currentChannel = channel;
System.out.println("Channel changed to: " + channel);
}
// Static method
public static void getManufacturerInfo() {
System.out.println("All TVs by: " + manufacturerLogo);
}
}
In this example:
currentChannel
, volume
, and isOn
fields are instance variables. Each TV set (object) has its own set of these variables.manufacturerLogo
field is a static
variable. It belongs to the class itself and is shared among all TV sets.changeChannel()
method is an instance method. Each TV set can invoke this method independently.getManufacturerInfo()
method is a static method. It belongs to the class and can be invoked without creating an instance of the Television
class.To access instance members, you need to create an instance of the class:
Television tv1 = new Television();
tv1.changeChannel(5); // Changes channel of tv1
But to access static members, you can use the class name directly:
Television.getManufacturerInfo();
Static members are useful for representing class-level data and behavior that is shared among all instances of the class. They can be accessed without creating an instance of the class, making them memory-efficient. However, static members cannot access instance members directly, as they are not associated with any specific instance.
It is important to note that Java allows static members (fields and methods) to be accessed through instances of a class. For example, the static method getManufacturerInfo()
can be used this way too:
tv1.getManufacturerInfo();
However, this is not recommended practice, as it does not clearly convey that the member is static and belongs to the class rather than the instance.
Instance members, on the other hand, are associated with each individual instance of the class. They hold data specific to each object and can access both static and instance members.
Now, you might be thinking: Why can static members be accessed without creating an instance of the class? Does this not go against the idea of object-oriented programming??
Well, this doesn’t necessarily go against the principles of object-oriented programming (OOP), but rather complements them by providing a mechanism for defining class-level behavior and state.
Static methods can be used to implement utility or helper functions that do not depend on the state of an object instance. This is common in utility classes, such as the Math
class, where all methods are static because they do not require access to instance-level data.
Also, static members allow for global access. Granted, there’s some controversy about this due to the potential for increased coupling and harder-to-test code, however, it can be appropriate for global constants that need to be accessed from various points in an application.
This diagram illustrates several key points about static and instance members in Java:
┌─────────────────────────────────────────────────────────────┐
│ Class │
│ ┌─────────────────────────┐ ┌─────────────────────────┐ │
│ │ Static Members │ │ Instance Members │ │
│ │ ┌─────────────────────┐ │ │ ┌─────────────────────┐ │ │
│ │ │ Static Fields │ │ │ │ Instance Fields │ │ │
│ │ └─────────────────────┘ │ │ └─────────────────────┘ │ │
│ │ ┌─────────────────────┐ │ │ ┌─────────────────────┐ │ │
│ │ │ Static Methods │ │ │ │ Instance Methods │ │ │
│ │ └─────────────────────┘ │ │ └─────────────────────┘ │ │
│ └─────────────────────────┘ └─────────────────────────┘ │
│ │
│ ┌─────────────────────────┐ ┌─────────────────────────┐ │
│ │ Object 1 │ │ Object 2 │ │
│ │ ┌─────────────────────┐ │ │ ┌─────────────────────┐ │ │
│ │ │ Instance Fields │ │ │ │ Instance Fields │ │ │
│ │ └─────────────────────┘ │ │ └─────────────────────┘ │ │
│ └─────────────────────────┘ └─────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
- The class contains both static and instance members.
- Static members (fields and methods) are associated with the class itself.
- Instance members (fields and methods) are associated with objects of the class.
- Multiple objects of the class each have their own instance members.
- All objects share the same static members.
Now, let’s review in more detail how to declare fields.
A field is a variable that is declared at the class level. Fields, which are also referred to as attributes or instance variables, are used to hold the state of an object.
To declare a field, you use the following syntax:
[accessModifier] [specifiers] type fieldName [= initialValue];
Here are some examples:
public class MyClass {
public static final int MAX_VALUE = 100;
private String name;
protected double salary;
boolean active = true;
// ...
}
The access modifier is optional and can be public
, private
, protected
or default (package-private) access if none is specified. Notice that unlike classes, fields can use all four types of access modifiers. Depending on the access modifiers used, fields can be accessed from inside the class, subclasses, classes in the same package or any other class. More on this later.
The specifiers part is also optional and can include keywords like static
, final
, transient
, and volatile
. You can specify zero or more specifiers (like in the first field declaration), but the final
keyword can only be applied once:
static
belongs to the class itself, not to a particular instance. There will only be one copy of a static
field shared by all instances of the class. A common use of static
fields is to define constants.final
cannot be reassigned to refer to a different object or value. If it’s a primitive type, the value cannot be modified. If it’s a reference type, the reference cannot be changed to point to another object, but the internal state of the object can be altered if it’s mutable. Final fields can be used for constants or to make fields read-only after initialization. However, while the field itself becomes read-only, objects referenced by final fields can still have their internal state changed if they are mutable.transient
and volatile
keywords are more advanced and relate to serialization and multi-threading. We’ll cover them in later chapters.The type of the field follows the specifiers. It can be a primitive type like int
, boolean
, etc. or a reference type like String
, LocalDate
, ArrayList
, etc.
The field name follows standard Java identifier naming rules. Here are the main rules you need to remember for field identifiers:
Unicode Characters: Java allows Unicode characters in identifiers, which means you can use characters from non-Latin character sets. However, this is not commonly used for field names, as it can make the code harder to read and maintain.
Characters Allowed: Field identifiers can only include alphanumeric characters (A-Z
, a-z
, 0-9
), underscore (_
), and dollar sign ($
). The identifier must begin with a letter (A-Z
or a-z
), underscore (_
), or dollar sign ($
). It cannot start with a digit.
No Reserved Words: Identifiers cannot be Java reserved words. Reserved words include keywords like int
, if
, class
, and so on. These are part of the Java language syntax and have specific meanings to the compiler.
Case Sensitivity: Java is case-sensitive, meaning identifiers like myField
, MyField
, and MYFIELD
would be considered distinct.
Unlimited Length: Technically, there is no limit to the length of an identifier, but it’s essential to keep it reasonable for readability and maintainability.
It’s important to differentiate between rules and conventions. Rules must be followed for the Java code to compile, while conventions, such as starting field names with a lowercase letter or using camelCase
for multiple words, are best practices designed to make the code more readable and maintainable but are not enforced by the compiler.
Finally, providing an initial value is optional. If none is provided, fields will be initialized with their default values (0
, false
or null
depending on the type). However, the initial value must be a compile-time constant for static final fields.
Once a field is declared, you can access it to read its value or modify it by assigning a new value. The way you access a field depends on whether it’s an instance field or a static field and what access modifier it uses.
To access an instance field, you first need an instance of the class. Then you can read the field’s value using the dot (.
) operator like this:
instanceVariable.fieldName
For example:
String name = person.firstName;
int age = employee.age;
To modify an instance field, you use the assignment operator (=
) like this:
person.firstName = "John";
employee.age = 45;
Accessing static
fields is a bit different. Since they belong to the class itself, you don’t need an instance. You can access a static field using the class name and the dot operator:
ClassName.fieldName
For example:
double pi = Math.PI;
int max = Integer.MAX_VALUE;
Inside the same class that declares a field, you can access it directly by its name, without any prefix, regardless of the access modifier used. The only exception is accessing a static field, it’s recommended to use the class name even within the same class for readability.
The access modifiers public
, private
, protected
and default(package) control the visibility of a field and determine whether it can be accessed directly from outside the class.
Let’s look at some examples to illustrate the different access levels.
public class Person {
public String name;
private int age;
protected String email;
double height;
}
The name
field is public
, so it can be accessed from any other class:
Person p = new Person();
p.name = "Alice";
The age
field is private
. It can only be accessed within the Person
class. Trying to access it directly from outside the class will result in a compile error:
// This will not compile
p.age = 30;
The email
field is protected
. It can be accessed within the same class, any subclass, and other classes in the same package:
// This is okay
String email = p.email;
// This is also valid in a subclass, even in a different package
class Employee extends Person {
public void setEmail(String e) {
email = e;
}
}
The height
field has default (package) access since no modifier is specified. It can be accessed by other classes within the same package:
// This is okay if Person and Student are in same package
class Student {
public void printHeight(Person p) {
System.out.println(p.height);
}
}
It’s common to declare fields as private
and access them through getter and setter methods. public
and protected
fields are used less frequently. Default (package-private) access is useful for related classes within the same package.
A method is a block of code that performs a specific task and optionally returns a value. Methods are used to define the behavior of an object. They provide a way to encapsulate complex logic, break down a program into manageable parts, and enable code reuse.
To declare a method, use the following syntax:
[accessModifier] [specifiers] returnType methodName([parameters]) [throws ExceptionType1, ExceptionType2, ...] {
// method body
}
For example:
public static String addParenthesis(String s) {
return "(" + s + ")";
}
private int sum(int a, int b) {
return a + b;
}
protected void setName(String name) throws IllegalArgumentException {
if (name == null || name.isEmpty()) {
throw new IllegalArgumentException("Name cannot be null or empty");
}
this.name = name;
}
The access modifier is optional and controls the visibility of the method. It can be public
, private
, protected
, or default (package) access if none is specified. The same rules apply as for fields, which we discussed earlier.
The specifiers are also optional and can include keywords like static
, final
, abstract
, and synchronized
. These keywords modify the behavior of the method:
static
methods belong to the class itself and can be called without an instance of the class.final
methods cannot be overridden by subclasses.abstract
methods have no implementation in the current class and must be overridden by non-abstract subclasses.synchronized
methods can only be executed by one thread at a time.The return type specifies the type of value the method returns. It can be a primitive type, a reference type, or void if the method doesn’t return anything. Every method declaration must have a return type.
The method name follows the same naming conventions as classes and fields, typically using camelCase
. Choose meaningful names that describe the purpose of the method.
The parameters are specified within parentheses after the method name. There can be zero or more parameters. Multiple parameters are separated by commas. Parameters are variables that receive the values passed to the method when it is called. Each parameter consists of two, optionally three, parts:
[parameterModifier] parameterType parameterName
The parameter modifier is optional and can be final
. If a parameter is declared as final
, it means that the value of the parameter cannot be changed inside the method body. Here’s an example:
public void printMessage(final String message) {
// message = "Hello"; // This would cause a compile error
System.out.println(message);
}
The parameter type is required and specifies the data type of the parameter. It can be a primitive type (like int
, double
, boolean
) or a reference type (like String
, ArrayList
, or custom classes).
The parameter name is also mandatory and follows the same naming conventions as class, fields and method identifiers, typically using camelCase
. The parameter name is used to refer to the passed value within the method body.
Here are a few examples of parameter definitions:
// A single parameter of type int
public void printNumber(int number) {
System.out.println("The number is: " + number);
}
// Multiple parameters of different types
public void printPersonDetails(String name, int age, boolean isStudent) {
System.out.println("Name: " + name);
System.out.println("Age: " + age);
System.out.println("Is a student? " + isStudent);
}
// A parameter with a modifier
public void calculateDiscount(final double price, double discountPercentage) {
double discountAmount = price * (discountPercentage / 100);
double finalPrice = price - discountAmount;
System.out.println("Discounted price: " + finalPrice);
}
Back to the parts of a method declaration, the throws
clause is optional and specifies any checked exceptions that the method might throw. Multiple exceptions are separated by commas.
The method body is enclosed in curly braces {}
and contains the code that implements the method’s functionality. It can include variable declarations, loops, conditionals, method calls, and other statements.
If the method has a return type other than void, it must include a return
statement that specifies the value to be returned. The return value must be compatible with the declared return type:
// A simple method that returns a string
public String getName() {
return "Mark";
}
A method signature uniquely identifies a method within a class. It consists of the method’s name and the ordered list of parameter types. The access modifiers (sucha as public
or private
), return types (such as void
or int
), and parameter names are not part of the method signature:
methodName(parameterType1, parameterType2, ...)
For example, consider the following method declarations:
public void printMessage(String message) {
System.out.println(message);
}
public int calculateSum(int a, int b) {
return a + b;
}
private void updateUser(String username, int age, boolean isActive) {
// method body
}
The method signatures for these methods are:
printMessage(String)
calculateSum(int, int)
updateUser(String, int, boolean)
When calling a method, you pass arguments that match the types and order of the parameters declared in the method signature. The arguments are the actual values that are passed to the method.
This way, to call a method, you need to use the method name followed by parentheses and provide any required arguments. The syntax is:
[ObjectReference.]methodName([arguments]);
If the method is an instance method (non-static), you need to have an object of the class that contains the method. You can then call the method using the object reference followed by the dot operator and the method name.
If the method is a static
method, you can call it directly using the class name followed by the dot operator and the method name. You don’t need an object instance to call a static method.
Here are a few examples of calling methods:
// Calling an instance method
Person person = new Person();
person.setName("John");
String name = person.getName();
// Calling a static method
int max = Math.max(10, 20);
double random = Math.random();
// Calling a method with arguments
Calculator calculator = new Calculator();
int sum = calculator.add(5, 3);
double result = calculator.multiply(2.5, 4.0);
Make sure to provide the correct number and type of arguments as defined in the method signature. If there is a mismatch, the compiler will throw an error.
Just like with fields, access modifiers control the visibility and accessibility of methods. The same four access modifiers can be used: public
, private
, protected
, and default (package-private).
Consider this class:
package com.my.package;
public class MathUtils {
public static int add(int a, int b) {
return a + b;
}
private static int subtract(int a, int b) {
return a - b;
}
protected static int multiply(int a, int b) {
return a * b;
}
static int divide(int a, int b) {
return a / b;
}
}
The add
method is declared as public
, so it can be called from any other class:
int sum = MathUtils.add(1, 2);
The subtract
method is declared as private
. It can only be called from within the MathUtils
class itself. Trying to call it from another class will result in a compile error:
// This will not compile
int difference = MathUtils.subtract(10, 7);
The multiply
method is declared as protected
. It can be called from within the same class, any subclass (even in a different package), and other classes in the same package:
package com.my.other.package;
// Calling from a subclass in a different package
public class AdvancedMathUtils extends MathUtils {
public static int square(int a) {
return multiply(a, a);
}
}
The divide
method has default (package-private) access since no explicit modifier is specified. Remember, this means that the method is accessible only to classes within the same package:
package com.my.package;
// Calling from another class in the same package
public class ArithmeticOperations {
public static int performDivision(int a, int b) {
return MathUtils.divide(a, b);
}
}
In Java, when you pass arguments to a method, they are always passed by value. This means that a copy of the value is passed to the method, rather than a reference to the original variable. However, the behavior of pass-by-value differs depending on whether you are passing a primitive type (like an int
) or a reference type (like an object such as String
).
When you pass a primitive type to a method, the method receives a copy of the value. Any changes made to the parameter inside the method do not affect the original variable outside the method.
Here’s an example:
public void testPrimitive() {
int num = 10;
modifyPrimitive(num);
System.out.println(num); // Output: 10
}
public void modifyPrimitive(int value) {
value = 20;
}
In this example, the modifyPrimitive
method receives a copy of the value of num
. Modifying the value
parameter inside the method does not change the original num
variable in the testPrimitive
method.
When you pass a reference type to a method, the method receives a copy of the reference to the object. While the reference itself is passed by value, the method can still modify the state of the object that the reference points to.
Here’s an example:
public void test() {
Person person = new Person("John", 25);
modifyPerson(person);
System.out.println(person.getName()); // Output: Alice
System.out.println(person.getAge()); // Output: 25
}
public void modifyPerson(Person p) {
p.setName("Alice"); // Sets a new name
p = new Person("Bob", 30); // Reassigns p to a new person
}
In this example, the modifyPerson
method receives a copy of the reference to the Person
object. Inside the method, the setName()
method is called on the object referenced by p
, which modifies the name of the original object. However, when p
is reassigned to a new Person
object, it does not affect the original person reference in the main
method.
Let’s explore a few more examples to clarify the difference between reassigning a reference and modifying the object itself.
First, consider this one about reassigning a reference:
public void test() {
StringBuilder sb = new StringBuilder("Hello");
modifyStringBuilder(sb);
System.out.println(sb.toString()); // Output: Hello
}
public void modifyStringBuilder(StringBuilder builder) {
builder = new StringBuilder("World");
}
In this example, the modifyStringBuilder
method receives a copy of the reference to the StringBuilder
object. Inside the method, the builder reference is reassigned to a new StringBuilder
object, but this does not affect the original sb
reference in the main
method.
Contrast the previous example with the following, which demonstrates how modifying the state of an object differs from simply reassigning a reference:
public void test() {
StringBuilder sb = new StringBuilder("Hello");
appendToStringBuilder(sb);
System.out.println(sb.toString()); // Output: Hello, World!
}
public void appendToStringBuilder(StringBuilder builder) {
builder.append(", World!");
}
In this example, the appendToStringBuilder
method receives a copy of the reference to the StringBuilder
object. Inside the method, the append()
method is called on the object referenced by builder, which modifies the state of the original object. The changes made to the object are visible outside the method.
Understanding the behavior of pass-by-value and the difference between reassigning a reference and modifying the object itself is important for writing correct and predictable code. Always consider whether you intend to modify the object or simply reassign the reference when passing reference types to methods.
In Java, it is possible to define two or more methods within the same class that share the same name, as long as their parameter declarations are different. This is called method overloading. Consider the methods of the following class:
public class Calculator {
public int add(int a, int b) {
return a + b;
}
public double add(double a, double b) {
return a + b;
}
}
When the add
method is called, the Java compiler determines which version of the overloaded method to call based on the type of the arguments passed to it.
This is similar to ordering coffee at a coffee shop. The barista can prepare different variations of coffee based on your specifications, black coffee, coffee with milk, or coffee with milk and sugar. Each variation is ordered using the same word (coffee), but the ingredients you specify determine the exact type of coffee you’ll receive. In the same way, when you call an overloaded method in Java, the arguments you pass determine which version of the method will be executed.
For example, if we call the add
method with different arguments:
Calculator calc = new Calculator();
int result1 = calc.add(5, 10);
System.out.println(result1); // Output: 15
double result2 = calc.add(5.5, 10.2);
System.out.println(result2); // Output: 15.7
double result3 = calc.add(5, 10.2);
System.out.println(result3); // Output: 15.2
This is what happens:
When calc.add(5, 10)
is called, both arguments are of type int
. The Java compiler matches this call with the add
method that takes two int
parameters, and the result is an int
value of 15.
When calc.add(5.5, 10.2)
is called, both arguments are of type double
. The Java compiler matches this call with the add
method that takes two double
parameters, and the result is a double
value of 15.7.
When calc.add(5, 10.2)
is called, one argument is an int
, and the other is a double
. In this case, the Java compiler performs a conversion of the int
argument to a double
to match the add
method that takes two double
parameters. The result is a double
value of 15.2.
This example demonstrates how the Java compiler uses the type of the arguments to determine which overloaded method to call. It matches the arguments with the most specific method signature available.
Java can only pick an overloaded method if it can find an exact match for the arguments or if it can find a version that is more specific through widening conversions.
Widening conversions are when you go from a smaller data type to a larger data type, for instance, from an int
to a long
, or, like in the above example, from an int
to a double
.
However, Java cannot apply narrowing conversions (going from a larger data type to a smaller one) automatically. If it doesn’t find an exact match or a match through widening, it will give a compile error.
It’s important to note that method overloading is not the same as method overriding. We’ll talk more about overriding in the next chapter, but when overriding, you provide a different implementation for an inherited method. The overridden method must have the same name, return type, and parameters as the inherited method. On the other hand, overloaded methods must have the same name but different parameters.
So always keep this in mind, changing just the return type is not enough for method overloading. The parameter list must be different.
Also, a common misconception is that Java always choose the overloaded method with the most parameters. It doesn’t. Java selects the method based on the most specific match to the argument types, not necessarily the method with the most parameters.
Consider this class that has multiple overloaded methods named display
:
public class DisplayOverload {
// Method with a single String argument
public void display(String str) {
System.out.println("Displaying a String: " + str);
}
// Overloaded method with a single int argument
public void display(int num) {
System.out.println("Displaying an integer: " + num);
}
// Overloaded method with two int arguments
public void display(int num1, int num2) {
System.out.println("Displaying two integers: " + num1 + " and " + num2);
}
}
// ...
DisplayOverload obj = new DisplayOverload();
obj.display("Hello, World!"); // Calls the method with a String argument
obj.display(5); // Calls the method with a single int argument
obj.display(10, 20); // Calls the method with two int arguments
In this example:
display("Hello, World!");
is called, Java selects the display(String str)
method because the argument is a String, which matches the parameter type of this specific method.display(5);
is called, Java selects the display(int num)
method because the argument is an integer, making it the most specific match among the overloaded methods.display(10, 20);
is called, even though there are other display
methods that could theoretically accept integers, Java picks display(int num1, int num2)
because it most specifically matches the provided two integer arguments.One last thing to note is that you cannot overload methods that differ only by a varargs parameter. For example, this will not compile:
public void sum(int[] numbers) { }
public void sum(int... numbers) { } // Compile-time error
The reason is that both int[] numbers
and int... numbers
are essentially the same from Java’s perspective because int...
is just syntactic sugar for an array of integers (int[]
). When you try to overload a method with these two parameter types, Java sees them as identical signatures. But let’s talk more about varargs.
Varargs, short for variable-length arguments, are a feature that allow methods to accept an unspecified number of arguments of a specific type. Think of varargs like an all-you-can-eat buffet. At a buffet, you’re not limited to a fixed number of dishes; you can choose to have as many different dishes as you want, and you can even go back for more. Similarly, with varargs, a method can be called with a varying number of arguments; you’re not fixed to a specific number. This makes your methods more flexible and easier to use when the exact number of inputs may vary.
To define a method with varargs, you use an ellipsis (...
) after the data type of the last parameter. Here’s how it works:
public void display(String... words) {
for (String word : words) {
System.out.println(word);
}
}
In this example, display
can be called with any number of String
arguments, including none at all. It’s as if you’re telling the method, “Here’s what I have, take it all.” This flexibility makes varargs extremely useful for creating methods that need to handle an unknown number of objects, like a list of names, numbers, or even complex objects.
Now, there are specific rules you must follow to use varargs effectively and correctly.
First, a varargs parameter must be the last parameter in a method’s parameter list. This rule ensures that the method can accept a variable number of arguments without ambiguity regarding which arguments belong to the varargs parameter and which do not. For instance, consider the following method:
void printStrings(String title, String... strings) {
System.out.println(title + ":");
for (String str : strings) {
System.out.println(str);
}
}
In this example, String... strings
is a varargs parameter that can accept any number of String
arguments. Being the last parameter allows you to call printStrings
with any number of strings, or even no strings at all.
Second, only one varargs parameter is allowed in a method’s parameter list. This restriction prevents confusion over which arguments belong to which varargs parameter if more than one were allowed. For example, if you wanted to create a method that sums numbers, you might do the following:
double multiplyAndSum(double multiplier, int... numbers) {
double sum = 0;
for (int num : numbers) {
sum += num;
}
return sum * multiplier;
}
This method correctly includes only one varargs parameter (int... numbers
), ensuring clarity in how it should be called and how it operates on the passed arguments.
Third, a method with a varargs parameter can be overloaded, but you have to make sure to avoid ambiguity. This requires ensuring that each method signature is distinct enough to prevent compile-time errors. For example, you could have:
void display(String s, int... numbers) {
System.out.println(s);
for (int num : numbers) {
System.out.print(num + " ");
}
System.out.println();
}
void display(String first, String second) {
System.out.println(first + ", " + second);
}
Here, display
is overloaded with one version accepting a string and a varargs integer parameter, and another accepting two strings. This overloading is valid because the method signatures are distinct, ensuring that the compiler can determine which method to call based on the arguments provided.
Inside the method, accessing the elements of a varargs parameter can be done in several ways, each suitable for different scenarios.
The simplest way to access elements in a varargs parameter is by treating it as an array and accessing its elements directly using an index. This method is useful when you know the exact number of arguments or need to access specific elements. For example, consider a method that prints the first, second, and last elements of a varargs parameter:
void printSelectedNumbers(int... numbers) {
if (numbers.length >= 3) {
System.out.println("First: " + numbers[0]);
System.out.println("Second: " + numbers[1]);
System.out.println("Last: " + numbers[numbers.length - 1]);
} else {
System.out.println("Insufficient arguments.");
}
}
This method directly accesses elements by their indices, similar to an array access, making it straightforward to retrieve specific values.
For iterating over each element in a varargs parameter, the enhanced for loop provides a clean and concise way to process each argument. This approach is most beneficial when you need to perform operations on every element or when the number of arguments is variable. Here’s an example that sums all numbers passed to the method:
int sumAll(int... numbers) {
int sum = 0;
for (int num : numbers) {
sum += num;
}
return sum;
}
The enhanced for
loop automatically iterates over each element in numbers
, allowing for easy aggregation or processing.
Although similar to using an enhanced for
loop, you might sometimes need to manually iterate over a varargs parameter using its length
property for more complex logic, such as when you need to access the current index. Here’s how you might print each element with its index:
void printWithIndices(String... strings) {
for (int i = 0; i < strings.length; i++) {
System.out.println("Element " + i + ": " + strings[i]);
}
}
This method leverages the length
property of the varargs parameter to manually control the iteration, offering flexibility for index-based operations.
For more complex operations, including filtering, mapping, or aggregating elements, Java’s Stream API can work directly with varargs. This method is particularly powerful for processing elements in functional programming style. We’ll cover streams in a later chapter, but, for instance, you can filter and sum only the even numbers as follows:
int sumEvenNumbers(int... numbers) {
return Arrays.stream(numbers) // Convert varargs to a stream
.filter(n -> n % 2 == 0) // Filter even numbers
.sum(); // Sum them
}
Once you have defined a method that takes a vararg parameter, you can call it by passing individual arguments, by passing an array, or by calling it without any arguments.
The most straightforward way to call a method with varargs is by passing individual arguments to it. This approach is identical to calling a method with a fixed number of parameters, but with the added flexibility of specifying any number of arguments. Here’s an example using a method that prints out each argument:
void printArgs(String... args) {
for (String arg : args) {
System.out.println(arg);
}
}
// Calling the method
printArgs("Hello", "World", "Varargs", "are", "flexible");
In this example, the printArgs
method is called with five string arguments, demonstrating the ease with which you can pass any number of arguments.
Alternatively, you can call a varargs method by passing an array of the specified type. This approach is useful when the arguments are already stored in an array, or when you wish to dynamically construct the list of arguments. Consider a method that sums an arbitrary number of integers:
int sumNumbers(int... numbers) {
return Arrays.stream(numbers).sum();
}
// Calling the method with an array
int[] numberArray = {1, 2, 3, 4, 5};
int sum = sumNumbers(numberArray);
System.out.println("Sum is: " + sum);
Here, sumNumbers
is called with an integer array, showcasing how an array matches the varargs signature, providing a compact way to pass multiple arguments.
Finally, a varargs method can also be called without passing any arguments. This feature is particularly useful when an operation is optional or when there is a valid default behavior in the absence of inputs. Here’s a method that concatenates any number of strings, with a demonstration of calling it without arguments:
String concatenateStrings(String... strings) {
return Stream.of(strings).collect(Collectors.joining(", "));
}
// Calling the method without arguments
String result = concatenateStrings();
System.out.println("Result: " + result);
The above example illustrates that calling concatenateStrings
without any arguments is perfectly valid and that varargs provide a flexible method signature that accommodates a wide range of use cases.
main
MethodThe main
method is a special method in Java that serves as the entry point of a Java application. When you run a Java program, the JVM looks for this method and starts executing the code inside it. Every Java application must have a main
method in at least one of its classes.
Here’s the syntax for declaring a main
method:
public static void main(String[] args) {
// ...
}
Let’s break down each part:
public
: The main
method must be declared as public
to allow the JVM to call it from outside the class.static
: The main
method must be declared as static
so that it can be called without creating an instance of the class.void
: The main
method doesn’t return any value, so its return type is void
.main
: The name of the method must be “main” (all lowercase) for the JVM to recognize it as the entry point.String[] args
: The main
method accepts a single parameter, of type String
array, conventionally named args
. This parameter allows you to pass command-line arguments to the program.Here’s an example of a simple main
method:
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello, World!");
}
}
In this example, the main
method simply prints Hello, World!
to the console.
The arguments of the program are passed as a String
array, where each element represents a separate argument:
public class CommandLineArguments {
public static void main(String[] args) {
if (args.length > 0) {
System.out.println("Arguments:");
for (String arg : args) {
System.out.println(arg);
}
} else {
System.out.println("No arguments provided.");
}
}
}
In this example, the main
method checks if any arguments were passed, using args.length
. If there are arguments, it iterates over the args
array and prints each argument. If no arguments were provided, it prints a message indicating that.
You can run this program from the command line and pass arguments like this:
java CommandLineArguments arg1 arg2 arg3
This will be the output:
Arguments:
arg1
arg2
arg3
If you run the program without any arguments:
java CommandLineArguments
This will be the output:
No arguments provided.
It’s possible to have multiple methods named main
in a class, as long as they have different parameter lists. However, only the method defined as public static void main(String[] args)
will be recognized as the entry point of the application:
public class MainOverloading {
public static void main(String[] args) {
System.out.println("Main method with String[] args");
main(42);
}
public static void main(int num) {
System.out.println("Main method with int parameter: " + num);
}
}
In this example, the class has two main
methods: one with the standard signature and another with an int parameter, however, the main(String[] args)
method is the entry point, and it calls the main(int num)
method.
This is the output:
Main method with String[] args
Main method with int parameter: 42
In Java, a constructor is a special method used to initialize objects. It is called when an instance of a class is created.
Imagine a constructor as a recipe for baking a specific cake. Just as the recipe contains the instructions and ingredients to make the cake, a constructor has the code to set up the initial state of an object.
The syntax to define a constructor is straightforward. It has the same name as the class and no return type, not even void.
Here’s an example:
class Cake {
String flavor;
double price;
Cake() {
flavor = "Vanilla";
price = 9.99;
}
}
To create an object, we use the new
keyword followed by a call to the constructor:
Cake myCake = new Cake();
The above line will create a new Cake
object with the default Vanilla
flavor and a price of 9.99
.
But what if you want the default flavor but a different price? Or customize both in certain cases?
Well, just as you can bake different varieties of cakes by tweaking the recipe, you can create objects with different initial states by providing multiple constructors.
For example, let’s add another constructor to our Cake
class:
Cake(String flavor, double price) {
this.flavor = flavor;
this.price = price;
}
With this constructor, we can create a cake with any flavor and price we want:
Cake specialCake = new Cake("Chocolate", 12.99);
Having multiple constructors gives flexibility in object creation. We can provide different ways to initialize an object based on the data available at the time of creation.
The constructor with no parameters is called the default constructor. If you don’t define any constructors in your class, the compiler will automatically provide a default constructor with an empty body.
However, if you define any constructor (like our parameterized one), the compiler will not provide a default constructor. In this case, if you still want the option to create an object without specifying parameters, you need to explicitly define the default constructor.
So in our Cake
class, we could have both constructors:
class Cake {
String flavor;
double price;
Cake() {
flavor = "Vanilla";
price = 9.99;
}
Cake(String flavor, double price) {
this.flavor = flavor;
this.price = price;
}
}
Now we can create a default vanilla cake with new Cake()
or a customized cake with new Cake("Chocolate", 10.99)
.
Instance initializers are blocks of code that are executed when an object is created, just like constructors. However, while constructors are methods with a specific name and potentially parameters, instance initializers are just code blocks within a class.
Let’s use an analogy to understand instance initializers.
Imagine moving into a new house. We all have our unique rituals to make a house feel like a home. Some might hang family photos, others might paint the walls in their favorite color. These rituals are specific to each person, just as instance initializers are specific to each object.
Here’s the syntax for an instance initializer:
class House {
String color;
// instance initializer
{
color = "White";
System.out.println("Performing move-in ritual");
}
}
Whenever a new House
object is created, the code inside the instance initializer block will run. It will set the color to White
and print "Performing move-in ritual"
.
So, how do instance initializers compare to constructors, and when might you use them?
Well, imagine you have a class with multiple constructors. Each constructor needs to perform some common initialization tasks. Instead of duplicating the code in each constructor, you can put it in an instance initializer. The initializer code will run regardless of which constructor is used.
class House {
String color;
int numberOfRooms;
// instance initializer
{
color = "White";
System.out.println("Performing move-in ritual");
}
House(int numberOfRooms) {
this.numberOfRooms = numberOfRooms;
}
House(String color, int numberOfRooms) {
this.color = color;
this.numberOfRooms = numberOfRooms;
}
}
In this case, regardless of which constructor is used to create a House
object, the instance initializer will run, setting the default color to White
and printing the move-in message.
However, it’s important to note that in most cases, you can achieve the same result by simply moving the common initialization code into a separate method and calling that method from each constructor.
In fact, some argue that instance initializers are redundant since anything you can do with an instance initializer, you can also do with a constructor. The main difference is that constructors can take parameters, while instance initializers cannot.
That said, there are some scenarios where instance initializers can be useful. For example, if you’re using anonymous classes (which we’ll cover later), you can’t define a constructor, so an instance initializer is your only option for initialization code.
Static initializers are blocks of code that are executed when a class is loaded into memory, before any instances of the class are created. They are used to initialize static variables or perform actions that are common to all instances of the class.
Imagine a town hall meeting that happens once when a town is established. In this meeting, the town’s leaders set up rules and guidelines that apply to everyone in the town. This one-time setup is similar to what a static
initializer does for a class.
Here’s the syntax for a static
initializer:
class TownHall {
static String townName;
static int population;
// static initializer
static {
townName = "JavaVille";
population = 1000;
System.out.println("Town established: " + townName);
}
}
The static
keyword before the opening brace denotes that this is a static initializer block. It will run once when the TownHall
class is loaded, setting the townName
to JavaVille
, the initial population
to 1000
, and printing Town established: JavaVille
.
Now, you might think that static initializers are just another way to initialize static variables, and you could achieve the same result by directly initializing the variables at their declaration, like this:
static String townName = "JavaVille";
static int population = 1000;
And you’d be partially correct. For simple initializations, direct assignment is often clearer and more concise.
However, static initializers provide more flexibility. They allow you to write more complex initialization logic, such as:
Here’s an example that demonstrates this:
static List<String> residents = new ArrayList<>();
static {
Path path = Paths.get("residents.txt");
try (Stream<String> lines = Files.lines(path)) {
lines.forEach(residents::add);
} catch (IOException e) {
System.out.println("Residents file not found.");
}
}
In this case, we’re using the static initializer to read a list of residents from a file and populate the residents
list. This kind of complex initialization would not be possible with a simple direct assignment.
Another key difference is that a class can have multiple static initializers, and they will be executed in the order they appear in the class. This can be useful for organizing complex initialization logic into readable chunks.
It’s important to note that static initializers are executed before any instance of the class is created, and even before the main
method is called. They are part of the class loading process.
In contrast, instance initializers and constructors are run every time a new instance of the class is created. They are part of the object creation process.
We have reviewed constructors, instance initializers, and static initializers. However, if a class includes all three, which one executes first? What is the order of initialization?
When a class is loaded, the first things to be initialized are the static variables and static initializers, in the order they appear in the class. This happens once per class loading, before any instances are created.
After that, whenever a new instance of the class is created, the instance variables are initialized, and the instance initializers and constructors are run.
The order is as follows:
0
, false
, or null
). This step ensures that all instance variables have a predictable starting state before any further initialization code is executed.Here’s an example that demonstrates this order:
class InitializationOrder {
static int staticVar = 1;
int instanceVar = 1;
static {
System.out.println("Static Initializer: staticVar = " + staticVar);
staticVar = 2;
}
{
System.out.println("Instance Initializer: instanceVar = " + instanceVar);
instanceVar = 2;
}
InitializationOrder() {
System.out.println("Constructor: instanceVar = " + instanceVar);
instanceVar = 3;
}
public static void main(String[] args) {
System.out.println("Creating new instance");
InitializationOrder obj = new InitializationOrder();
System.out.println("Created instance: instanceVar = " + obj.instanceVar);
}
}
If you run this code, the output will be:
Static Initializer: staticVar = 1
Creating new instance
Instance Initializer: instanceVar = 1
Constructor: instanceVar = 2
Created instance: instanceVar = 3
Let’s break this down:
InitializationOrder
class is loaded, the static variable staticVar
is initialized to 1
, and then the static initializer is run, which prints the current value of staticVar
(1
) and then sets it to 2
.main
method, we print Creating new instance
to mark the start of instance creation.InitializationOrder
object is created. First, the instance variable instanceVar
is initialized to its default value of 1
.instanceVar
(1
) and then sets it to 2
.instanceVar
(2
) and then sets it to 3
.main
method, we print the final value of instanceVar
(3
).It’s important to keep this order in mind, especially if your initializers and constructors depend on each other. Incorrect assumptions about initialization order can lead to subtle bugs.
Also, note that if a class has multiple static initializers, they will run in the order they appear in the class. The same is true for instance initializers.
Let’s extend our previous example to demonstrate this:
class MultipleInitializers {
static int staticVar1;
static int staticVar2;
int instanceVar1;
int instanceVar2;
static {
System.out.println(
"Static Initializer 1: staticVar1 = " + staticVar1
);
staticVar1 = 1;
}
static {
System.out.println(
"Static Initializer 2: staticVar2 = " + staticVar2
);
staticVar2 = 2;
}
{
System.out.println(
"Instance Initializer 1: instanceVar1 = " + instanceVar1
);
instanceVar1 = 1;
}
{
System.out.println(
"Instance Initializer 2: instanceVar2 = " + instanceVar2
);
instanceVar2 = 2;
}
MultipleInitializers() {
System.out.println("Constructor");
}
public static void main(String[] args) {
System.out.println("Creating new instance");
MultipleInitializers obj = new MultipleInitializers();
System.out.println(
"Created instance: instanceVar1 = "
+ obj.instanceVar1
+ ", instanceVar2 = "
+ obj.instanceVar2
);
}
}
When we run this code, the output will be:
Static Initializer 1: staticVar1 = 0
Static Initializer 2: staticVar2 = 0
Creating new instance
Instance Initializer 1: instanceVar1 = 0
Instance Initializer 2: instanceVar2 = 0
Constructor
Created instance: instanceVar1 = 1, instanceVar2 = 2
Here’s what’s happening:
When the MultipleInitializers
class is loaded, the static variables staticVar1
and staticVar2
are initialized to their default value of 0
.
The first static initializer is run, which prints the current value of staticVar1
(0
) and then sets it to 1
.
The second static initializer is run, which prints the current value of staticVar2
(0
) and then sets it to 2
.
In the main
method, we print Creating new instance
to mark the start of instance creation.
A new MultipleInitializers
object is created. First, the instance variables instanceVar1
and instanceVar2
are initialized to their default value of 0
.
The first instance initializer is run, which prints the current value of instanceVar1
(0
) and then sets it to 1
.
The second instance initializer is run, which prints the current value of instanceVar2
(0) and then sets it to 2
.
The constructor is executed, which simply prints Constructor
.
Finally, back in the main
method, we print the final values of instanceVar1
(1
) and instanceVar2
(2
).
Remember, all static initializers will run before any instance initializers, and all initializers will run before the constructor. But within each category (static or instance), the initializers will run in the order they are defined in the class.
java.lang.Object
In Java, every class is implicitly a subclass of the java.lang.Object
class, which is the root of the class hierarchy. Even if you don’t explicitly extend any class, your class will automatically inherit from Object
.
The Object
class provides a set of fundamental methods that are common to all objects. When you create a new class, you automatically inherit these methods. Some of the commonly used methods inherited from Object
include:
toString()
: Returns a string representation of the object. By default, it returns a string consisting of the object’s class name, an @
symbol, and the object’s hash code in hexadecimal format. You can override this method to provide a custom string representation of your object.
equals(Object obj)
: Compares the object with another object for equality. By default, it compares the object references using the ==
operator. You can override this method to define custom equality logic based on the object’s state.
hashCode()
: Returns a hash code value for the object. The hash code is used in hash-based data structures such as HashSet
and HashMap
. By default, it returns a unique integer value for each object. If you override the equals()
method, you should also override the hashCode()
method to ensure that equal objects have the same hash code.
getClass()
: Returns the runtime class of the object. It is a final
method, which means it cannot be overridden.
clone()
: Creates and returns a copy of the object. By default, it performs a shallow copy of the object. To use this method, your class must implement the Cloneable
interface.
Here’s an example that demonstrates some of the methods inherited from Object
:
class MyClass {
private int value;
public MyClass(int value) {
this.value = value;
}
@Override
public String toString() {
return "MyClass[value=" + value + "]";
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null || getClass() != obj.getClass())
return false;
MyClass other = (MyClass) obj;
return value == other.value;
}
@Override
public int hashCode() {
return Objects.hash(value);
}
}
In this example, the MyClass
overrides the toString()
, equals()
, and hashCode()
methods inherited from Object
. The toString()
method provides a custom string representation of the object, the equals()
method defines equality based on the value field, and the hashCode()
method generates a hash code based on the value field.
By leveraging these methods, you can provide meaningful string representations, define equality comparisons, and ensure proper behavior in hash-based data structures.
In Java, it’s possible to define a class within another class. Such classes are called nested classes. Similar to how a box can contain several smaller boxes inside it, a class (the outer or enclosing class) can have other classes (nested classes) defined within it.
There are four types of nested classes in Java:
Static nested class
Inner class (also known as a non-static nested class)
Local class
Anonymous class
Each type of nested class has its own characteristics and use cases:
A static nested class is like a smaller box that doesn’t depend on the larger box for its existence. It can access the static members of the enclosing class directly. However, to access non-static members, it needs an instance of the enclosing class, just like any other external class would.
An inner class, on the other hand, is like a smaller box that is closely tied to the larger box. It can access both static and non-static members of the enclosing class directly. However, an instance of an inner class cannot exist without an instance of the enclosing class.
A local class is like a temporary box created within a method or a block of the enclosing class. The scope of a local class is confined to the block in which it is defined. While local classes do not use traditional access modifiers like public
or private
, their accessibility is inherently limited to the enclosing block.
An anonymous class is like a one-time, nameless box created for a specific purpose. It is defined and instantiated in a single statement, usually as an argument to a method call or as an initializer. The accessibility of an anonymous class is determined by the context in which it is used, such as within a method or as a field initializer, and does not use traditional access modifiers.
When deciding between a static nested class and an inner class, consider the relationship between the nested class and the enclosing class. If the nested class doesn’t need access to the non-static members of the enclosing class, use a static nested class. This makes the class more independent and reusable. If the nested class requires access to the non-static members of the enclosing class or needs to be tied to an instance of the enclosing class, use an inner class.
It’s important to note that while nested classes can help organize code better, they do impact how the code works. Each type of nested class has its own specific behavior and use cases. For example, an inner class has an implicit reference to an instance of the enclosing class, which can have implications for memory usage and serialization.
A common misconception is that static nested classes and inner classes are essentially the same since they are both defined within another class. However, this is not true. Static nested classes are semantically similar to any other top-level class and do not have an implicit reference to an instance of the enclosing class. Inner classes, on the other hand, are intimately tied to an instance of the enclosing class and cannot exist independently.
In terms of access modifiers, nested classes can be declared as public
, package-private (default), protected
, or private
, unlike top-level classes that can’t be declared as protected
or private
.
The accessibility of static and non-static nested classes depends on its access modifier and the accessibility of the enclosing class. For example, if the enclosing class is public
and the nested class is private
, the nested class can only be accessed within the enclosing class. If the nested class is public, it can be accessed from anywhere, provided the enclosing class is also accessible.
Here’s a bit more detail on each type:
Public Nested Class: A public
nested class is accessible from any other class, but the accessibility of the nested class still depends on the accessibility of its outer class. If the outer class is not accessible in some context, then its nested public class won’t be accessible there either.
Protected Nested Class: A protected
nested class is accessible within its own package and by subclasses of its outer class, regardless of the package the subclass is in. This allows for more controlled visibility compared to a public nested class, particularly useful when you want to expose certain functionality only to certain subclasses.
Private Nested Class: A private
nested class is accessible only within its outer class. This is useful for hiding the class from the outside world completely, making it accessible only to the outer class. This is often used for helper classes that are of no interest outside of the outer class.
Package-Private (Default) Nested Class: A nested class with no access modifier is package-private, meaning it is accessible only within its own package. This is the default access level if no access modifier is specified. It’s a middle ground in terms of accessibility, more restrictive than public
but less restrictive than private
.
Local classes are defined in a block, typically within a method body. The visibility of a local class is restricted to the block in which it is defined. Thus, while you cannot apply traditional access modifiers (public
, protected
, private
) to the class itself because it is not visible outside the block, you can control the access to instances of this class from within the block.
And since anonymous classes are used within an expression, they don’t allow access modifiers for the class itself. The context in which they are declared dictates their accessibility. However, the methods and fields within an anonymous class can have access modifiers, subject to normal scoping rules.
Here’s a summary table of the allowed access modifiers for each type of nested class:
Nested Class Type | public |
protected |
default |
private |
---|---|---|---|---|
Static Nested Class | Yes | Yes | Yes | Yes |
Inner Class | Yes | Yes | Yes | Yes |
Local Class | No | No | Yes* | No |
Anonymous Class | No | No | Yes* | No |
Yes
indicates that the access modifier is allowed.No
indicates that the access modifier is not applicable.Yes*
indicates that for local and anonymous classes the concept of traditional access modifiers does not apply to these classes because their visibility is inherently confined to the block in which they are declared. Therefore, they do not have access modifiers in the traditional sense.Now let’s go over each type in more detail.
A static nested class is a class defined within another class and marked with the static
keyword:
class OuterClass {
static class StaticNestedClass {
// members of the static nested class
}
}
Static nested classes can be declared with any of the four access modifiers: public
, protected
, package-private (default), or private
. The accessibility of the static nested class depends on the access modifier used and the accessibility of the enclosing class. Here’s an example:
public class OuterClass {
private static class PrivateNestedClass {
// ...
}
protected static class ProtectedNestedClass {
// ...
}
static class PackagePrivateNestedClass {
// ...
}
public static class PublicNestedClass {
// ...
}
}
In this example, PrivateNestedClass
is accessible only within OuterClass
, ProtectedNestedClass
is accessible within OuterClass
and its subclasses, PackagePrivateNestedClass
is accessible within the same package as OuterClass
, and PublicNestedClass
is accessible from anywhere.
Static nested classes can extend another class and implement interfaces, just like any other top-level class:
class BaseClass {
// ...
}
interface MyInterface {
// ...
}
class OuterClass {
static class NestedClass extends BaseClass implements MyInterface {
// ...
}
}
Here, NestedClass
extends BaseClass
and implements MyInterface
, demonstrating that a static nested class can extend another class and implement interfaces.
They can access the static members of the enclosing class directly, using the name of the enclosing class followed by the dot notation. However, to access non-static members of the enclosing class, a static nested class requires an instance of the enclosing class. This is because static nested classes do not inherently have access to the instance variables of the enclosing class.
Here’s an example of a nested static class:
class OuterClass {
private static int staticField = 10;
private int instanceField = 20;
static class NestedClass {
void accessOuterMembers() {
System.out.println(staticField); // Accessible directly
System.out.println(instanceField); // Compilation error: cannot access non-static field
System.out.println(new OuterClass().instanceField); // Accessible via an instance of OuterClass
}
}
}
In this example, NestedClass
can directly access the staticField
of OuterClass
, but it cannot directly access the instanceField
. To access instanceField
, it needs an instance of OuterClass
.
To create an instance of a static nested class, you don’t need an instance of the enclosing class. You can instantiate it using the name of the enclosing class followed by the dot notation and the name of the static nested class.
OuterClass.StaticNestedClass nestedObject = new OuterClass.StaticNestedClass();
When referencing static members of a static nested class from outside the enclosing class, use the enclosing class’s name, followed by a dot, the static nested class’s name, another dot, and then the member’s name. This syntax highlights the nested structure while providing clear paths to access static members:
OuterClass.StaticNestedClass.staticField;
OuterClass.StaticNestedClass.staticMethod();
OuterClass.StaticNestedClass.StaticNestedNestedClass nestedNestedObject
= new OuterClass.StaticNestedClass.StaticNestedNestedClass();
From within the enclosing class, you can directly access the members of the static nested class without the name of the enclosing class.
class OuterClass {
static class StaticNestedClass {
static void staticMethod() {
// ...
}
}
void outerMethod() {
StaticNestedClass.staticMethod();
}
}
As you can see, static nested classes are similar to regular, top-level classes in many ways:
They can have all types of access modifiers (public
, private
, protected
, and package).
They can extend other classes and implement interfaces.
They can have static and non-static members.
They can be instantiated independently (without an instance of the enclosing class).
However, there are a few key differences:
Static nested classes are defined within another class, whereas top-level classes are defined independently.
Static nested classes have access to the static members of the enclosing class directly, while top-level classes need to use the enclosing class name to access its static members.
Static nested classes can be private
, allowing for better encapsulation, whereas top-level classes can only be public
or package-private.
In summary, static nested classes are essentially like regular top-level classes that have been nested within another class for organizational purposes. They do not have an implicit reference to an instance of the enclosing class and can be instantiated independently. This makes them useful for grouping related classes together and providing a level of encapsulation.
Non-static nested classes, also known as inner classes, are classes that are defined within another class without the static
keyword:
class OuterClass {
class InnerClass {
// members of the inner class
}
}
An inner class can be declared with any of the four access modifiers: public
, protected
, private
, or the default access level. The accessibility of the inner class depends on the access modifier used and the accessibility of the enclosing class. If the outer class is public
and the inner class is private
, the inner class can only be accessed within the outer class. Here’s an example:
public class OuterClass {
private class PrivateInnerClass {
// ...
}
protected class ProtectedInnerClass {
// ...
}
class PackagePrivateInnerClass {
// ...
}
public class PublicInnerClass {
// ...
}
}
In this example, PrivateInnerClass
is accessible only within OuterClass
, ProtectedInnerClass
is accessible within OuterClass
and its subclasses, PackagePrivateInnerClass
is accessible within the same package as OuterClass
, and PublicInnerClass
is accessible from anywhere, provided OuterClass
is accessible.
An inner class can extend another class and implement interfaces, just like any other class. This allows inner classes to inherit behavior and conform to contracts defined by other classes and interfaces:
class BaseClass {
// ...
}
interface MyInterface {
// ...
}
class OuterClass {
class InnerClass extends BaseClass implements MyInterface {
// ...
}
}
Here, InnerClass
extends BaseClass
and implements MyInterface
, demonstrating that an inner class can inherit from another class and conform to an interface.
An inner class has access to all members (fields, methods, and nested classes) of the enclosing class, including private
members. This is because an inner class is associated with an instance of the outer class and shares a special relationship with it. The inner class can directly access and manipulate the state of the outer class instance:
class OuterClass {
private int privateField = 10;
protected int protectedField = 20;
int packagePrivateField = 30;
public int publicField = 40;
class InnerClass {
void accessOuterMembers() {
System.out.println(privateField);
System.out.println(protectedField);
System.out.println(packagePrivateField);
System.out.println(publicField);
}
}
}
In this example, InnerClass
has direct access to all members of OuterClass
, including the private field privateField
. The inner class can freely access and manipulate the state of the outer class instance.
To create an instance of an inner class, you typically need an instance of the outer class. The most common way to instantiate an inner class is from within a non-static method of the outer class:
class OuterClass {
class InnerClass {
// ...
}
void outerMethod() {
InnerClass innerObject = new InnerClass();
}
}
From outside the outer class, you can instantiate an inner class using the following syntax:
OuterClass outerObject = new OuterClass();
OuterClass.InnerClass innerObject = outerObject.new InnerClass();
To reference members (fields, methods, nested classes) of an inner class from outside the outer class, you first need an instance of the outer class, then use the dot notation to access the inner class, followed by another dot and the member name:
OuterClass outerObject = new OuterClass();
OuterClass.InnerClass innerObject = outerObject.new InnerClass();
innerObject.innerField;
innerObject.innerMethod();
From within the outer class, you can directly access the members of the inner class using an instance of the inner class:
class OuterClass {
class InnerClass {
void innerMethod() {
// ...
}
}
void outerMethod() {
InnerClass innerObject = new InnerClass();
innerObject.innerMethod();
}
}
Inner classes differ from regular, top-level classes in several ways:
Inner classes are defined within another class, whereas top-level classes are defined outside of other classes.
Inner classes have access to all members of the enclosing class, including private
members, while top-level classes can only access public
and protected
members of other classes.
Inner classes are associated with an instance of the outer class and cannot exist independently, while top-level classes can be instantiated independently.
Inner classes can be private
, allowing for better encapsulation, whereas top-level classes can only be public
or package-private.
Inner classes are useful when a class is closely tied to another class and needs access to its internals. They provide a way to organize related classes and maintain a tight coupling between them. Inner classes are commonly used for implementing event listeners, iterators, or other functionality that is specific to the enclosing class.
Local classes are defined within a block of code, typically within a method or a constructor. They have limited scope and are only accessible within the block where they are defined:
void someMethod() {
class LocalClass {
// members of the local class
}
}
A local class cannot have any access modifiers. They cannot be accessed from outside the block or method in which they are defined. This is because local classes are not members of the enclosing class, but rather defined within a method or block.
However, they can extend another class and implement interfaces, just like any other class.
Example:
void someMethod() {
class LocalClass extends BaseClass implements MyInterface {
// ...
}
}
Also, a local class has access to all members (fields, methods, and nested classes) of the enclosing class, including private
members. Additionally, a local class can access final
or effectively final local variables and parameters of the enclosing method:
class OuterClass {
private int privateField = 10;
void someMethod(final int parameter) {
final int localVariable = 20;
class LocalClass {
void accessOuterMembers() {
System.out.println(privateField);
System.out.println(parameter);
System.out.println(localVariable);
}
}
LocalClass localObject = new LocalClass();
localObject.accessOuterMembers();
}
}
In this example, LocalClass
has access to the private
field privateField
of OuterClass
, as well as the final
parameter parameter
and the final local variable localVariable
of the someMethod()
.
To create an instance of a local class, you can instantiate it within the method or block where it is defined, using the new
keyword:
void someMethod() {
class LocalClass {
// ...
}
LocalClass localObject = new LocalClass();
}
To reference members (fields, methods, nested classes) of a local class, you can directly access them using an instance of the local class within the method or block where it is defined:
void someMethod() {
class LocalClass {
int localField = 10;
void localMethod() {
System.out.println("Local method");
}
}
LocalClass localObject = new LocalClass();
System.out.println(localObject.localField);
localObject.localMethod();
}
Local classes differ from regular, top-level classes in several ways:
Local classes are defined within a method or block, whereas top-level classes are defined independently.
Local classes have limited scope and are only accessible within the block where they are defined, while top-level classes have a broader scope.
Local classes cannot have access modifiers, while top-level classes can be public
or package-private.
Local classes can access final
or effectively final local variables and parameters of the enclosing method, while top-level classes cannot directly access local variables or parameters.
Local classes are useful when you need to define a class that is only used within a specific method or block and does not need to be accessed from other parts of the code. They provide a way to encapsulate behavior and state within a limited scope.
Anonymous classes are a way to define and instantiate a class at the same time, without giving it a name. They are used for creating one-time implementations of interfaces or abstract classes.
To declare an anonymous class, you use the new
keyword followed by the name of an interface or an abstract class, and then provide the class body in curly braces.
interface MyInterface {
void myMethod();
}
MyInterface myObject = new MyInterface() {
@Override
public void myMethod() {
// Implementation of myMethod()
}
};
Since anonymous classes are not explicitly named and are defined at the point of use, they cannot have any explicit access modifiers. Their accessibility is determined by the context in which they are used. Specifically, the scope in which an anonymous class is defined determines its accessibility. For instance, if an anonymous class is defined within a method, it is accessible only within that method. If it is defined within a class, it follows the accessibility rules of that class.
An anonymous class can extend a class or implement an interface, however, it cannot do both at the same time:
class BaseClass {
void baseMethod() {
System.out.println("Base method");
}
}
interface MyInterface {
void myMethod();
}
BaseClass anonymousObject1 = new BaseClass() {
@Override
void baseMethod() {
System.out.println("New implementation of base method");
}
};
MyInterface anonymousObject2 = new MyInterface() {
@Override
public void myMethod() {
System.out.println("Implementation of myMethod()");
}
};
An anonymous class has access to all members (fields, methods, and nested classes) of the enclosing class, including private
members. Additionally, an anonymous class can access final
or effectively final
local variables and parameters of the enclosing method:
class OuterClass {
private int privateField = 10;
void someMethod(final int parameter) {
final int localVariable = 20;
MyInterface anonymousObject = new MyInterface() {
@Override
public void myMethod() {
System.out.println(privateField);
System.out.println(parameter);
System.out.println(localVariable);
}
};
anonymousObject.myMethod();
}
}
An anonymous class does not have a name, so you cannot directly reference its members from outside the class body. However, you can reference the members of the interface or abstract class that the anonymous class implements or extends:
interface MyInterface {
void myMethod();
int myField = 10;
}
MyInterface anonymousObject = new MyInterface() {
@Override
public void myMethod() {
System.out.println("Implementation of myMethod()");
}
};
anonymousObject.myMethod();
System.out.println(MyInterface.myField);
Anonymous classes differ from regular, top-level classes in several ways:
Anonymous classes are defined and instantiated at the same time, without an explicit name, whereas top-level classes are defined separately and instantiated using the new
keyword.
Anonymous classes are defined at the point of use, typically as an argument to a method or as an initializer, while top-level classes are defined independently.
Anonymous classes cannot have explicit access modifiers, constructors, or static members, while top-level classes can have all of these.
Anonymous classes are used for creating one-time implementations or instances, while top-level classes are used for creating reusable and named classes.
In summary, anonymous classes are useful when you need to create a one-time implementation of an interface or abstract class without the need for a named class. They provide a concise way to define and instantiate a class in a single expression.
Finally, to wrap up this section, here’s a table that summarizes many properties of each type of nested class:
Property | Static Nested Class | Inner Class | Local Class | Anonymous Class |
---|---|---|---|---|
Association with Outer Class | Loosely associated (can exist without an instance of the outer class) | Tightly coupled (cannot exist without an instance of the outer class) | Tightly coupled (associated with an instance of the enclosing block) | Tightly coupled (instantiated within an expression and associated with an instance of the enclosing block) |
Can Declare Static Members | Yes (including static methods and fields) | No (except final static fields) | No (cannot declare static members, only final variables) | No (cannot declare static members, only final variables) |
Access to Members of the Outer Class | Only static members | Both static and instance members | Both static and instance members | Both static and instance members |
Requires Reference to Outer Class Instance | No | Yes | Yes (implicitly final or effectively final variables from the enclosing scope) | Yes (implicitly final or effectively final variables from the enclosing scope) |
Typical Use Cases | Grouping classes that are used in only one place, enhancing encapsulation | Handling events, accessing private members of the outer class, providing more readable and maintainable code | Encapsulating complex code within a method without making it visible outside | Simplifying the instantiation of objects that are meant to be used once or where the class definition is unnecessary |
It’s important to note that you can have one or more class definitions in one Java source file. However, you should follow these rules:
If a Java class is declared as public
, the name of the file must exactly match the name of the public class, including case sensitivity, with the addition of the .java
extension. For example, if you have a public
class named MyClass
, then the source file must be named MyClass.java
:
// File name: MyClass.java
public class MyClass {
// class body
}
A Java source file can contain multiple classes, but it can only have one public
class. If there are multiple classes in a file and one of them is declared public
, the file name must match the name of the public
class. For example, if PublicClass
is the public
class, the file must be named PublicClass.java
, and it can also contain AnotherClass
which is not public
:
// File name: PublicClass.java
public class PublicClass {
// class body
}
class AnotherClass {
// class body
}
If there’s no public
class in the file, any name can be used. For example, the following file, ManyClasses.java
contains multiple classes, none of which are public
:
// File name: ManyClasses.java
class FirstClass {
// class body
}
class SecondClass {
// class body
}
If multiple non-public classes exist in a single file, then the file name does not need to match the name of any of the classes. For example, you can have a file named UtilityClasses.java
containing multiple non-public classes that don’t match this name:
// File name: UtilityClasses.java
class HelperClass {
// class body
}
class AnotherHelperClass {
// class body
}
Java is case-sensitive. If your class is named CaseSensitiveClass
, the file name must match exactly (CaseSensitiveClass.java
):
// File name: CaseSensitiveClass.java
public class CaseSensitiveClass {
// class body
}
So the main restriction in Java is that a source file cannot contain more than one public
class. This helps in organizing code and making it easier to manage. Each public class must be in its own source file, and the file name must match the class name (including case sensitivity) with the .java
extension.
However, a single Java source file can contain any number of non-public classes. These classes are by default package-private, and the file can also contain protected
or private
nested classes within public
or package-private classes. This flexibility allows for logically related classes to be grouped together within the same file if they are not intended for public
use, aiding in encapsulation and modular design.
Object-oriented programming (OOP) organizes code into objects, which represent real-world entities containing data (attributes) and behaviors (methods).
Classes are blueprints or templates that define the data and behaviors common to all objects of that type, while objects are distinct instances of a class containing unique data values.
The main stages of an object’s life-cycle in Java are creation using the new
keyword, accessing via reference variables, and cleanup by Java’s garbage collector when no longer referenced.
Keywords are reserved words in Java that define the structure and syntax of Java programs. They cannot be used as identifiers.
Comments are annotations in the code ignored by the compiler, used to describe or explain code. Java supports single-line (//
), multi-line (/* */
), and documentation (/** */
) comments.
Packages organize related classes, interfaces, and sub-packages into a single unit, providing a level of access control. The package
keyword is used to create a package.
Access modifiers (public
, protected
, default
, private
) control the visibility and accessibility of classes, methods, and variables from other parts of a Java application.
A class is declared using the class
keyword followed by the class name. It can optionally extend a superclass using extends
and implement interfaces using implements
.
Fields are variables declared at the class level to hold the state of an object. They can have access modifiers, specifiers (static
, final
), a type, and an optional initial value.
Methods are blocks of code that perform specific tasks and optionally return values. They are declared with an optional access modifier, specifiers, return type, name, parameters, and a method body.
Method overloading is the practice of defining multiple methods with the same name but different parameter lists within the same class.
The Java compiler determines which overloaded method to call based on the number, types, and order of the arguments passed during the method invocation.
Java can only pick an overloaded method if it can find an exact match for the arguments or if it can find a more specific version through widening conversions (int
to long
, int
to double
, etc.).
Varargs (variable-length arguments) allow methods to accept an unspecified number of arguments of a specific type. To define a method with varargs, use an ellipsis (…) after the data type of the last parameter in the method signature.
A varargs parameter must be the last parameter in a method’s parameter list, and only one varargs parameter is allowed per method.
Methods with varargs can be overloaded, but you must avoid ambiguity by ensuring the method signatures are different.
Constructors are special methods used to initialize objects, called when an instance of a class is created using the new
keyword. They have the same name as the class and no return type.
Instance initializers are blocks of code executed when an object is created, similar to constructors but without parameters. They are enclosed in {}
within the class body.
Static initializers are blocks of code executed when a class is loaded into memory, before any instances are created. They are defined using the static
keyword followed by {}
.
Every class implicitly extends the java.lang.Object
class, inheriting fundamental methods like toString()
, equals()
, and hashCode()
.
Nested classes are classes defined within another class. They can be static nested classes, inner classes (non-static nested classes), local classes, or anonymous classes.
Static nested classes are associated with the outer class itself and can access its static members directly. They can be instantiated independently, without an instance of the outer class.
Inner classes (non-static nested classes) are associated with an instance of the outer class and have access to both static and non-static members of the outer class. They require an instance of the outer class to be instantiated.
Local classes are defined within a block, typically a method, and have access to final or effectively final variables from the enclosing scope. They cannot have access modifiers and are only visible within the defining block.
Anonymous classes are defined within an expression and are used for creating one-time implementations of interfaces or abstract classes. They do not have a name and are instantiated at the point of declaration.
If a class is declared public
, the name of the Java source file must exactly match the name of the public
class, including case sensitivity, with the .java
extension.
A Java source file can contain multiple class definitions, but only one of them can be declared public
. If there is no public
class, the file name can be different from the class names.
1. Consider the following code snippet:
public class Main {
public static void main(String[] args) {
StringBuilder sb1 = new StringBuilder("Java");
StringBuilder sb2 = new StringBuilder("Python");
sb1 = sb2;
// More code here
}
}
After the execution of the above code, which of the following statements is true regarding garbage collection?
A) Both sb1
and sb2
are eligible for garbage collection.
B) Only the StringBuilder
object initially referenced by sb1
is eligible for garbage collection.
C) Only the StringBuilder
object initially referenced by sb2
is eligible for garbage collection.
D) Neither of the StringBuilder
objects are eligible for garbage collection.
2. Which of the following are reserved keywords in Java? (Choose all that apply.)
A) implement
B) array
C) volatile
D) extends
3. Consider the following code snippet:
1. // calculates the sum of numbers
2. public class Calculator {
3. /* Adds two numbers
4. * @param a the first number
5. * @param b the second number
6. * @return the sum of a and b
7. */
8. public int add(int a, int b) {
9. // return the sum
10. return a + b;
11. }
12. //TODO: Implement subtract method
13.}
Which of the following statements are true about the comments in the above code? (Choose all that apply.)
A) Line 1 is an example of a single-line comment.
B) Lines 3-7 demonstrate the use of a javadoc comment.
C) Line 9 uses a javadoc comment to explain the add
method.
D) Line 12 uses a special TODO
comment, different from a single-line comment.
E) Lines 3-7 is a block comment that is used as if it were a javadoc comment.
4. Consider you have the following two Java files located in the same directory:
// File 1: Calculator.java
package math;
public class Calculator {
public int add(int a, int b) {
return a + b;
}
}
// File 2: Application.java
package app;
import math.Calculator;
public class Application {
public static void main(String[] args) {
Calculator calc = new Calculator();
System.out.println(calc.add(5, 3));
}
}
Which of the following statements is true regarding the package
and import
statements in Java?
A) The import
statement in Application.java
is unnecessary because both classes are in the same directory.
B) The import
statement in Application.java
is necessary for using the Calculator
class because they belong to different packages.
C) The Calculator
class will not be accessible in Application.java
due to being in a different directory.
D) Removing the package
statement from both files will allow Application.java
to use Calculator
without an import
statement, regardless of directory structure.
5. Consider the default access levels provided by Java’s four access modifiers: public
, protected
, default
(no modifier), and private
. Which of the following statements correctly describe the access levels granted by these modifiers? (Choose all that apply.)
A) A public
class or member can be accessed by any other class in the same package or in any other package.
B) A protected
member can be accessed by any class in its own package, but from outside the package, only by classes that extend the class containing the protected
member.
C) A member with default
(no modifier) access can be accessed by any class in the same package but not from a class in a different package.
D) A private
member can be accessed only by methods that are members of the same class or within the same file.
E) A protected
member can be accessed by any class in the Java program, regardless of package.
6. Which of the following class declarations correctly demonstrates the use of access modifiers, class
keyword, and class naming conventions in Java?
A) class public Vehicle { }
B) public class vehicle { }
C) Public class Vehicle { }
D) public class Vehicle { }
E) classVehicle public { }
7. Consider the following code snippet:
public class Counter {
public static int COUNT = 0;
public Counter() {
COUNT++;
}
public static void resetCount() {
COUNT = 0;
}
public int getCount() {
return COUNT;
}
}
Which of the following statements are true about static
and instance members within the Counter
class? (Choose all that apply.)
A) The COUNT
variable can be accessed directly using the class name without creating an instance of Counter
.
B) The getCount()
method is an example of a static method because it returns the value of a static variable.
C) Every time a new instance of Counter
is created, the COUNT
variable is incremented.
D) The resetCount()
method resets the COUNT
variable to 0 for all instances of Counter
.
8. Which of the following are valid field name identifiers in Java? (Choose all that apply.)
A) int _age;
B) double 2ndValue;
C) boolean is_valid;
D) String $name;
E) char #char;
9. Consider the syntax used to declare methods in a class. Which of the following method declarations is correct according to Java syntax rules?
A) int public static final computeSum(int num1, int num2)
B) private void updateRecord(int id) throws IOException
C) synchronized boolean checkStatus [int status]
D) float calculateArea() {}
10. Given the method declarations below, which of them have the same method signature?
A) public void update(int id, String value)
B) private void update(int identifier, String data)
C) public boolean update(String value, int id)
D) void update(String value, int id)
E) protected void update(int id, int value) throws IOException
11. Given this class:
public class AccountManager {
private void resetAccountPassword(String accountId) {
// Implementation code here
}
void auditTrail(String accountId) {
// Implementation code here
}
protected void notifyAccountChanges(String accountId) {
// Implementation code here
}
public void updateAccountInformation(String accountId) {
// Implementation code here
}
}
Which of the following statements correctly describe the accessibility of the methods within the AccountManager
class from a class in the same package and from a class in a different package?
A) The resetAccountPassword
method can be accessed from any class within the same package but not from a class in a different package.
B) The auditTrail
method can be accessed from any class within the same package and from subclasses in different packages.
C) The notifyAccountChanges
method can be accessed from any class within the same package and from subclasses in different packages.
D) The updateAccountInformation
method can be accessed from any class, regardless of its package.
12. What will be the output of this program?
public class TestPassByValue {
public static void main(String[] args) {
int originalValue = 10;
TestPassByValue test = new TestPassByValue();
System.out.println("Before calling changeValue: " + originalValue);
test.changeValue(originalValue);
System.out.println("After calling changeValue: " + originalValue);
}
public void changeValue(int value) {
value = 20;
}
}
A)
Before calling changeValue: 10
After calling changeValue: 20
B)
Before calling changeValue: 10
After calling changeValue: 10
C)
Before calling changeValue: 20
After calling changeValue: 20
D)
Before calling changeValue: 20
After calling changeValue: 10
13. What will be the output of the following program?
public class Test {
public static void main(String[] args) {
print(null);
}
public static void print(Object o) {
System.out.println("Object");
}
public static void print(String s) {
System.out.println("String");
}
}
A) Object
B) String
C) Compilation fails
D) A runtime exception is thrown
14. Which of the following method declarations correctly uses varargs? Choose all that apply.
A) public void print(String... messages, int count)
B) public void print(int count, String... messages)
C) public void print(String messages...)
D) public void print(String[]... messages)
E) public void print(String... messages, String lastMessage)
15. Given the class Vehicle
:
public class Vehicle {
private String type;
private int maxSpeed;
public Vehicle(String type) {
this.type = type;
}
public Vehicle(int maxSpeed) {
this.maxSpeed = maxSpeed;
}
// Additional methods here
}
Which of the following statements is true regarding its constructors?
A) The class Vehicle
demonstrates constructor overloading by having multiple constructors with different parameter lists.
B) The class Vehicle
will compile with an error because it does not provide a default constructor.
C) It is possible to create an instance of Vehicle
with both type
and maxSpeed
initialized.
D) Calling either constructor will initialize both type
and maxSpeed
fields of the Vehicle
class.
16. Consider the following class with an instance initializer block:
public class Library {
private int bookCount;
private List<String> books;
{
books = new ArrayList<>();
books.add("Book 1");
books.add("Book 2");
// Instance initializer block
}
public Library(int bookCount) {
this.bookCount = bookCount + books.size();
}
public int getBookCount() {
return bookCount;
}
// Additional methods here
}
Given the Library
class above, which of the following statements accurately describe the role and effect of the instance initializer block?
A) The instance initializer block is executed before the constructor, initializing the books
list and adding two books to it.
B) The instance initializer block replaces the need for a constructor in the Library
class.
C) Instance initializer blocks cannot initialize instance variables like books
.
D) If multiple instances of Library
are created, the instance initializer block will execute each time before the constructor, ensuring the books
list is initialized and populated for each object.
17. Consider the following Java class with a static
initializer block:
public class Configuration {
private static Map<String, String> settings;
static {
settings = new HashMap<>();
settings.put("url", "https://eherrera.net");
settings.put("timeout", "30");
// Static initializer block
}
public static String getSetting(String key) {
return settings.get(key);
}
// Additional methods here
}
Given the Configuration
class above, which of the following statements accurately describe the role and effect of the static
initializer block?
A) The static
initializer block is executed only once when the class is first loaded into memory, initializing the settings
map with default values.
B) The static
initializer block allows instance methods to modify the settings
map without creating an instance of the Configuration
class.
C) static
initializer blocks are executed each time a new instance of the Configuration
class is created.
D) The static
initializer block is executed before any instance initializer blocks or constructors, when an instance of the class is created.
18. Consider the following class definition:
public class InitializationOrder {
static {
System.out.println("1. Static initializer");
}
private static int staticValue = initializeStaticValue();
private int instanceValue = initializeInstanceValue();
{
System.out.println("3. Instance initializer");
}
public InitializationOrder() {
System.out.println("4. Constructor");
}
private static int initializeStaticValue() {
System.out.println("2. Static value initializer");
return 0;
}
private int initializeInstanceValue() {
System.out.println("3. Instance value initializer");
return 0;
}
public static void main(String[] args) {
new InitializationOrder();
}
}
When the main
method of the InitializationOrder
class is executed, what is the correct order of execution for the initialization blocks, method calls, and constructor?
A)
1. Static initializer
2. Static value initializer
3. Instance initializer
3. Instance value initializer
4. Constructor
B)
1. Static initializer
2. Static value initializer
3. Instance value initializer
3. Instance initializer
4. Constructor
C)
1. Static initializer
3. Instance initializer
2. Static value initializer
3. Instance value initializer
4. Constructor
D)
2. Static value initializer
1. Static initializer
3. Instance value initializer
3. Instance initializer
4. Constructor
19. Consider a class CustomObject
that does not explicitly override any methods from java.lang.Object
:
public class CustomObject {
// Class implementation goes here
}
Which of the following statements correctly reflect the outcomes when methods from java.lang.Object
are used with instances of CustomObject
? (Choose all that apply.)
A) Invoking toString()
on an instance of CustomObject
will return a String
that includes the class name followed by the @
symbol and the object’s hashcode.
B) Calling equals(Object obj)
on two different instances of CustomObject
that have identical content will return true
because they are instances of the same class.
C) Using hashCode()
on any instance of CustomObject
will generate a unique integer that remains consistent across multiple invocations within the same execution of a program.
D) The clone()
method can be used to create a shallow copy of an instance of CustomObject
without the need for CustomObject
to implement the Cloneable
interface.
20. Consider the code snippet below that demonstrates the use of a static nested class:
public class OuterClass {
private static String message = "Hello, World!";
static class NestedClass {
void printMessage() {
// Note: A static nested class can access the static members of its outer class.
System.out.println(message);
}
}
public static void main(String[] args) {
OuterClass.NestedClass nested = new OuterClass.NestedClass();
nested.printMessage();
}
}
Which of the following statements is true regarding static nested classes in Java?
A) A static nested class can access both static and non-static members of its enclosing class directly.
B) Instances of a static nested class can exist without an instance of its enclosing class.
C) A static nested class can only be instantiated within the static method of its enclosing class.
D) Static nested classes are not considered members of their enclosing class and cannot access any members of the enclosing class.
21. Consider the following code snippet that demonstrates the use of a non-static nested (inner) class:
public class OuterClass {
private String message = "Hello, World!";
class InnerClass {
void printMessage() {
System.out.println(message);
}
}
public static void main(String[] args) {
OuterClass outer = new OuterClass();
OuterClass.InnerClass inner = outer.new InnerClass();
inner.printMessage();
}
}
Which of the following statements is true regarding non-static nested (inner) classes in Java?
A) A non-static nested class can directly access both static and non-static members of its enclosing class.
B) Instances of a non-static nested class can exist independently of an instance of its enclosing class.
C) A non-static nested class cannot access the non-static members of its enclosing class directly.
D) Non-static nested classes must be declared static to access the static members of their enclosing class.
22. Consider the following code snippet demonstrating the use of a local class within a method:
public class LocalClassExample {
public void printEvenNumbers(int[] numbers, int max) {
class EvenNumberPrinter {
public void print() {
for (int number : numbers) {
if (number % 2 == 0 && number <= max) {
System.out.println(number);
}
}
}
}
EvenNumberPrinter printer = new EvenNumberPrinter();
printer.print();
}
public static void main(String[] args) {
LocalClassExample example = new LocalClassExample();
int[] numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
example.printEvenNumbers(numbers, 6);
}
}
Which of the following statements correctly describe local classes in Java, based on the example provided?
A) Local classes can be declared within any block that precedes a statement.
B) Instances of a local class can be created and used outside of the block where the local class is defined.
C) Local classes are a type of static nested class and can access both static and non-static members of the enclosing class directly.
D) Local classes can access local variables and parameters of the enclosing block only if they are declared final
or effectively final.
23. Consider the following Java code snippet demonstrating the use of an anonymous class:
public class HelloWorld {
interface HelloWorldInterface {
void greet();
}
public void sayHello() {
HelloWorldInterface myGreeting = new HelloWorldInterface() {
@Override
public void greet() {
System.out.println("Hello, world!");
}
};
myGreeting.greet();
}
public static void main(String[] args) {
new HelloWorld().sayHello();
}
}
Which of the following statements is true about anonymous classes in Java?
A) Anonymous classes can implement interfaces and extend classes without the need to declare a named class.
B) An anonymous class must override all methods in the superclass or interface it declares it is implementing or extending.
C) Anonymous classes can have constructors as named classes do.
D) Instances of anonymous classes cannot be passed as arguments to methods.
24. Which of the following statements accurately reflects a valid rule regarding how classes and source files are organized?
A) A source file can contain multiple public classes.
B) Private classes can be declared at the top level in a source file.
C) A public
class must be declared in a source file that has the same name as the class.
D) If a source file contains more than one class, none of the classes can be public
.
Do you like what you read? Would you consider?
Do you have a problem or something to say?