Understanding how to use a comparator in Java is essential for customizing sorting behavior in your applications, which is where compare.edu.vn comes in, offering comprehensive comparisons. This guide provides a deep dive into Java comparators, comparison logic and custom sorting strategies. Discover how to implement and leverage comparators effectively with sort algorithms.
1. What Is A Comparator In Java And Why Use It?
A comparator in Java is an interface used to define a custom comparison logic for objects, enabling you to sort them based on specific criteria; it’s a crucial element for custom sorting strategies. Unlike the Comparable
interface, which dictates the natural ordering of a class, Comparator
allows for multiple sorting strategies to be applied externally, offering flexibility in various sorting scenarios. For example, if you’re working with a list of Student
objects, you might want to sort them by name, GPA, or ID, each requiring a different comparison approach.
1.1. The Essence Of The Comparator Interface
The Comparator
interface, found in the java.util
package, plays a pivotal role in Java’s sorting mechanism, enabling developers to define custom comparison logic outside of the class whose instances are being sorted. This separation of concerns is particularly beneficial when you need multiple sorting strategies for a single class or when you can’t modify the class itself. A comparator object compares two objects of the same class, returning an integer that indicates their relative order. The interface’s primary method is:
public int compare(Object obj1, Object obj2);
This method’s return value determines the order:
- A negative integer indicates that
obj1
is less thanobj2
. - Zero signifies that
obj1
is equal toobj2
. - A positive integer means that
obj1
is greater thanobj2
.
1.2. Why Opt For A Comparator?
Choosing a Comparator
over implementing the Comparable
interface within a class offers several advantages:
- Multiple Sorting Strategies:
Comparator
allows you to define various sorting approaches for the same class. For instance, you can sort a list ofEmployee
objects by salary, name, or hire date using different comparators. - External Sorting Logic: It keeps the sorting logic separate from the class, adhering to the single responsibility principle. This is particularly useful when you want to sort objects of a class that you cannot modify.
- Flexibility and Reusability: Comparators can be easily reused across different parts of your application. They can also be combined to create more complex sorting criteria.
- Use with Anonymous Classes and Lambda Expressions: Comparators can be implemented using anonymous classes or, more elegantly, with lambda expressions in Java 8 and later, making the code more concise and readable.
1.3. Scenarios Where Comparator Shines
Consider a scenario where you have a Product
class with attributes like name
, price
, and rating
. You might want to sort products by price, then by rating if prices are the same. A comparator allows you to define this complex sorting logic. Similarly, when dealing with collections of objects from external libraries that do not implement Comparable
, a comparator becomes essential for sorting. For instance, sorting a list of Book
objects from a third-party API by their publication year or author.
1.4. Comparator in Action
Let’s illustrate with an example. Suppose you have a Movie
class with title
and year
attributes. Here’s how you can create a comparator to sort movies by year:
import java.util.Comparator;
public class Movie {
private String title;
private int year;
public Movie(String title, int year) {
this.title = title;
this.year = year;
}
public String getTitle() { return title; }
public int getYear() { return year; }
@Override
public String toString() {
return title + " (" + year + ")";
}
public static class YearComparator implements Comparator<Movie> {
@Override
public int compare(Movie m1, Movie m2) {
return Integer.compare(m1.getYear(), m2.getYear());
}
}
}
In this example, YearComparator
implements the Comparator<Movie>
interface, providing a custom comparison logic based on the movie’s year. This comparator can then be used with the Collections.sort()
method or the sort()
method of a List
to sort a collection of Movie
objects by their release year. This method helps you avoid object comparison issues.
2. Diving Into Comparator Implementation
To effectively leverage the Comparator
interface in Java, understanding its implementation nuances is essential. This involves knowing the core methods to implement, how to handle different data types, and ways to optimize your comparator for performance.
2.1. Core Methods To Implement
The Comparator
interface primarily involves implementing the compare(Object obj1, Object obj2)
method. This method dictates the logic for comparing two objects of the same type. As previously mentioned, the return value of this method determines the order of the objects:
- A negative integer if
obj1
should come beforeobj2
. - Zero if
obj1
is equal toobj2
. - A positive integer if
obj1
should come afterobj2
.
In addition to compare()
, the Comparator
interface also includes an equals(Object obj)
method. However, it is often not necessary to explicitly implement this method, as the default implementation inherited from the Object
class typically suffices. The key is to ensure that your compare()
method provides a consistent and meaningful comparison.
2.2. Handling Different Data Types
When implementing a comparator, you’ll often encounter different data types that require specific handling. Here are a few examples:
-
Strings: For comparing strings, you can use the
compareTo()
method available in theString
class. This method compares two strings lexicographically.public int compare(String s1, String s2) { return s1.compareTo(s2); }
-
Numbers: For numeric types (e.g.,
Integer
,Double
), you can use theInteger.compare()
orDouble.compare()
methods, which provide a null-safe and efficient way to compare numbers.public int compare(Integer num1, Integer num2) { return Integer.compare(num1, num2); }
-
Dates: When comparing dates, use the
Date.compareTo()
method or the methods in thejava.time
package (introduced in Java 8), which offer a more modern and flexible approach to date and time manipulation.import java.time.LocalDate; public int compare(LocalDate date1, LocalDate date2) { return date1.compareTo(date2); }
-
Custom Objects: For custom objects, you need to access the specific attributes you want to compare and use the appropriate comparison method for those attributes. This might involve a combination of the above techniques.
public class Person { private String name; private int age; // Constructor and getters public static class AgeComparator implements Comparator<Person> { @Override public int compare(Person p1, Person p2) { return Integer.compare(p1.getAge(), p2.getAge()); } } }
2.3. Optimizing Comparator Performance
Optimizing comparator performance is crucial when dealing with large datasets or performance-sensitive applications. Here are some strategies to consider:
-
Minimize Object Access: Accessing object attributes can be costly, especially if the getter methods involve complex computations. Cache the attribute values in local variables within the
compare()
method to minimize repeated access. -
Use Primitive Comparisons: Primitive type comparisons (e.g.,
int
,double
) are generally faster than object comparisons. If possible, compare primitive attributes directly. -
Avoid Complex Logic: Keep the comparison logic as simple as possible. Complex computations or conditional statements can slow down the comparison process.
-
Leverage Existing Comparators: If you need to compare objects based on multiple attributes, consider combining existing comparators using the
thenComparing()
method introduced in Java 8. This can be more efficient than writing a complex custom comparator.import java.util.Comparator; public class Person { private String name; private int age; // Constructor and getters public static class NameAgeComparator implements Comparator<Person> { @Override public int compare(Person p1, Person p2) { return Comparator.comparing(Person::getName) .thenComparing(Person::getAge) .compare(p1, p2); } } }
-
Use Lambda Expressions Wisely: While lambda expressions can make your code more concise, they might introduce a slight performance overhead compared to traditional anonymous classes. Use them judiciously, especially in performance-critical sections.
By understanding these implementation nuances and optimization strategies, you can create efficient and effective comparators that meet the specific needs of your Java applications. Remember, the choice of the right comparison logic and optimization techniques can significantly impact the performance of your sorting operations.
3. Sorting Collections With Comparators
Once you have a comparator defined, the next step is to use it to sort collections of objects. Java provides several ways to sort collections using comparators, offering flexibility and control over the sorting process.
3.1. Using Collections.Sort()
The Collections.sort()
method is a convenient way to sort lists using a comparator. This method is part of the java.util.Collections
class and can be used to sort List
implementations like ArrayList
and LinkedList
.
Here’s the basic syntax:
Collections.sort(list, comparator);
list
: The list you want to sort.comparator
: An instance of a class that implements theComparator
interface.
Let’s illustrate with an example. Suppose you have a list of Book
objects and a TitleComparator
that compares books by their titles:
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
public class Book {
private String title;
private String author;
public Book(String title, String author) {
this.title = title;
this.author = author;
}
public String getTitle() { return title; }
public String getAuthor() { return author; }
@Override
public String toString() {
return title + " by " + author;
}
public static class TitleComparator implements Comparator<Book> {
@Override
public int compare(Book b1, Book b2) {
return b1.getTitle().compareTo(b2.getTitle());
}
}
public static void main(String[] args) {
List<Book> books = new ArrayList<>();
books.add(new Book("The Lord of the Rings", "J.R.R. Tolkien"));
books.add(new Book("Pride and Prejudice", "Jane Austen"));
books.add(new Book("1984", "George Orwell"));
System.out.println("Before sorting:");
books.forEach(System.out::println);
Collections.sort(books, new TitleComparator());
System.out.println("nAfter sorting by title:");
books.forEach(System.out::println);
}
}
In this example, the Collections.sort()
method is used to sort the list of Book
objects by their titles using the TitleComparator
.
3.2. Using List.Sort()
Java 8 introduced the sort()
method directly into the List
interface. This method provides a more streamlined way to sort lists using comparators.
Here’s the syntax:
list.sort(comparator);
list
: The list you want to sort.comparator
: An instance of a class that implements theComparator
interface.
Using the same Book
example, here’s how you can sort the list using the List.sort()
method:
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
public class Book {
private String title;
private String author;
public Book(String title, String author) {
this.title = title;
this.author = author;
}
public String getTitle() { return title; }
public String getAuthor() { return author; }
@Override
public String toString() {
return title + " by " + author;
}
public static class TitleComparator implements Comparator<Book> {
@Override
public int compare(Book b1, Book b2) {
return b1.getTitle().compareTo(b2.getTitle());
}
}
public static void main(String[] args) {
List<Book> books = new ArrayList<>();
books.add(new Book("The Lord of the Rings", "J.R.R. Tolkien"));
books.add(new Book("Pride and Prejudice", "Jane Austen"));
books.add(new Book("1984", "George Orwell"));
System.out.println("Before sorting:");
books.forEach(System.out::println);
books.sort(new TitleComparator());
System.out.println("nAfter sorting by title:");
books.forEach(System.out::println);
}
}
The List.sort()
method is generally preferred over Collections.sort()
because it’s more concise and directly available on the List
interface.
3.3. Sorting With Lambda Expressions
Java 8 also introduced lambda expressions, which provide a more concise way to define comparators directly within the sort()
method. This can make your code more readable and maintainable.
Here’s how you can use lambda expressions to sort the list of Book
objects by title:
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
public class Book {
private String title;
private String author;
public Book(String title, String author) {
this.title = title;
this.author = author;
}
public String getTitle() { return title; }
public String getAuthor() { return author; }
@Override
public String toString() {
return title + " by " + author;
}
public static void main(String[] args) {
List<Book> books = new ArrayList<>();
books.add(new Book("The Lord of the Rings", "J.R.R. Tolkien"));
books.add(new Book("Pride and Prejudice", "Jane Austen"));
books.add(new Book("1984", "George Orwell"));
System.out.println("Before sorting:");
books.forEach(System.out::println);
books.sort((b1, b2) -> b1.getTitle().compareTo(b2.getTitle()));
System.out.println("nAfter sorting by title:");
books.forEach(System.out::println);
}
}
In this example, the lambda expression (b1, b2) -> b1.getTitle().compareTo(b2.getTitle())
defines the comparison logic directly within the sort()
method.
3.4. Sorting With Multiple Criteria
Sometimes, you need to sort a collection based on multiple criteria. For example, you might want to sort a list of Employee
objects first by salary and then by name. Java 8 provides the thenComparing()
method in the Comparator
interface, which allows you to chain multiple comparison criteria.
Here’s an example:
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
public class Employee {
private String name;
private double salary;
public Employee(String name, double salary) {
this.name = name;
this.salary = salary;
}
public String getName() { return name; }
public double getSalary() { return salary; }
@Override
public String toString() {
return name + " (" + salary + ")";
}
public static void main(String[] args) {
List<Employee> employees = new ArrayList<>();
employees.add(new Employee("Alice", 50000));
employees.add(new Employee("Bob", 60000));
employees.add(new Employee("Charlie", 50000));
employees.add(new Employee("David", 60000));
System.out.println("Before sorting:");
employees.forEach(System.out::println);
employees.sort(Comparator.comparing(Employee::getSalary)
.thenComparing(Employee::getName));
System.out.println("nAfter sorting by salary and then by name:");
employees.forEach(System.out::println);
}
}
In this example, the employees
list is sorted first by salary using Comparator.comparing(Employee::getSalary)
and then by name using thenComparing(Employee::getName)
. This ensures that employees with the same salary are sorted alphabetically by name.
By mastering these techniques, you can effectively sort collections of objects using comparators, tailoring the sorting process to meet the specific needs of your Java applications.
4. Advanced Comparator Techniques
Beyond the basics, several advanced techniques can enhance your use of comparators in Java, providing greater flexibility and control over sorting operations. These include handling null values, using reverse order comparators, and combining comparators for complex sorting scenarios.
4.1. Handling Null Values
When dealing with collections that may contain null values, it’s essential to handle these nulls gracefully in your comparators to avoid NullPointerException
errors. Java provides several utilities to assist with this.
-
Comparator.nullsFirst()
: This method returns a comparator that considersnull
values as smaller than non-null values. When sorting in ascending order,null
values will appear at the beginning of the sorted collection.import java.util.ArrayList; import java.util.Comparator; import java.util.List; public class Product { private String name; private Double price; public Product(String name, Double price) { this.name = name; this.price = price; } public String getName() { return name; } public Double getPrice() { return price; } @Override public String toString() { return name + " (" + price + ")"; } public static void main(String[] args) { List<Product> products = new ArrayList<>(); products.add(new Product("Laptop", 1200.0)); products.add(new Product("Keyboard", 75.0)); products.add(new Product("Mouse", null)); products.add(new Product("Monitor", 300.0)); System.out.println("Before sorting:"); products.forEach(System.out::println); products.sort(Comparator.comparing(Product::getPrice, Comparator.nullsFirst(Comparator.naturalOrder()))); System.out.println("nAfter sorting by price with nulls first:"); products.forEach(System.out::println); } }
In this example,
Comparator.nullsFirst(Comparator.naturalOrder())
ensures that the product with anull
price is placed at the beginning of the sorted list. -
Comparator.nullsLast()
: This method returns a comparator that considersnull
values as larger than non-null values. When sorting in ascending order,null
values will appear at the end of the sorted collection.import java.util.ArrayList; import java.util.Comparator; import java.util.List; public class Product { private String name; private Double price; public Product(String name, Double price) { this.name = name; this.price = price; } public String getName() { return name; } public Double getPrice() { return price; } @Override public String toString() { return name + " (" + price + ")"; } public static void main(String[] args) { List<Product> products = new ArrayList<>(); products.add(new Product("Laptop", 1200.0)); products.add(new Product("Keyboard", 75.0)); products.add(new Product("Mouse", null)); products.add(new Product("Monitor", 300.0)); System.out.println("Before sorting:"); products.forEach(System.out::println); products.sort(Comparator.comparing(Product::getPrice, Comparator.nullsLast(Comparator.naturalOrder()))); System.out.println("nAfter sorting by price with nulls last:"); products.forEach(System.out::println); } }
In this case,
Comparator.nullsLast(Comparator.naturalOrder())
ensures that the product with anull
price is placed at the end of the sorted list.
4.2. Reverse Order Comparators
Sometimes, you need to sort a collection in reverse order. Java provides the reversed()
method in the Comparator
interface to easily achieve this.
-
Comparator.reverseOrder()
: This method returns a comparator that imposes the reverse of the natural ordering on a collection of objects that implement theComparable
interface.import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; public class Book implements Comparable<Book> { private String title; private String author; public Book(String title, String author) { this.title = title; this.author = author; } public String getTitle() { return title; } public String getAuthor() { return author; } @Override public String toString() { return title + " by " + author; } @Override public int compareTo(Book other) { return this.title.compareTo(other.title); } public static void main(String[] args) { List<Book> books = new ArrayList<>(); books.add(new Book("The Lord of the Rings", "J.R.R. Tolkien")); books.add(new Book("Pride and Prejudice", "Jane Austen")); books.add(new Book("1984", "George Orwell")); System.out.println("Before sorting:"); books.forEach(System.out::println); books.sort(Comparator.reverseOrder()); System.out.println("nAfter sorting in reverse order by title:"); books.forEach(System.out::println); } }
In this example,
Comparator.reverseOrder()
is used to sort the list ofBook
objects in reverse alphabetical order by title. -
comparator.reversed()
: If you have a custom comparator, you can use thereversed()
method to obtain a comparator that reverses the order defined by your custom comparator.import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; public class Book { private String title; private String author; public Book(String title, String author) { this.title = title; this.author = author; } public String getTitle() { return title; } public String getAuthor() { return author; } @Override public String toString() { return title + " by " + author; } public static class TitleComparator implements Comparator<Book> { @Override public int compare(Book b1, Book b2) { return b1.getTitle().compareTo(b2.getTitle()); } } public static void main(String[] args) { List<Book> books = new ArrayList<>(); books.add(new Book("The Lord of the Rings", "J.R.R. Tolkien")); books.add(new Book("Pride and Prejudice", "Jane Austen")); books.add(new Book("1984", "George Orwell")); System.out.println("Before sorting:"); books.forEach(System.out::println); books.sort(new TitleComparator().reversed()); System.out.println("nAfter sorting in reverse order by title:"); books.forEach(System.out::println); } }
In this case,
new TitleComparator().reversed()
creates a comparator that sorts the list ofBook
objects in reverse alphabetical order by title, using the customTitleComparator
.
4.3. Combining Comparators
For complex sorting scenarios, you can combine multiple comparators using the thenComparing()
method. This allows you to define a sorting order based on multiple criteria.
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
public class Employee {
private String name;
private double salary;
private int age;
public Employee(String name, double salary, int age) {
this.name = name;
this.salary = salary;
this.age = age;
}
public String getName() { return name; }
public double getSalary() { return salary; }
public int getAge() { return age; }
@Override
public String toString() {
return name + " (" + salary + ", " + age + ")";
}
public static void main(String[] args) {
List<Employee> employees = new ArrayList<>();
employees.add(new Employee("Alice", 50000, 30));
employees.add(new Employee("Bob", 60000, 25));
employees.add(new Employee("Charlie", 50000, 35));
employees.add(new Employee("David", 60000, 25));
System.out.println("Before sorting:");
employees.forEach(System.out::println);
employees.sort(Comparator.comparing(Employee::getSalary)
.thenComparing(Employee::getAge)
.thenComparing(Employee::getName));
System.out.println("nAfter sorting by salary, age, and name:");
employees.forEach(System.out::println);
}
}
In this example, the employees
list is sorted first by salary, then by age, and finally by name. This ensures that employees with the same salary and age are sorted alphabetically by name.
4.4. Using Comparators With Streams
Java 8 introduced streams, which provide a powerful way to process collections of data. You can use comparators with streams to sort data as part of a stream pipeline.
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
public class Product {
private String name;
private double price;
public Product(String name, double price) {
this.name = name;
this.price = price;
}
public String getName() { return name; }
public double getPrice() { return price; }
@Override
public String toString() {
return name + " (" + price + ")";
}
public static void main(String[] args) {
List<Product> products = new ArrayList<>();
products.add(new Product("Laptop", 1200.0));
products.add(new Product("Keyboard", 75.0));
products.add(new Product("Mouse", 25.0));
products.add(new Product("Monitor", 300.0));
System.out.println("Before sorting:");
products.forEach(System.out::println);
List<Product> sortedProducts = products.stream()
.sorted(Comparator.comparing(Product::getPrice))
.collect(Collectors.toList());
System.out.println("nAfter sorting by price using streams:");
sortedProducts.forEach(System.out::println);
}
}
In this example, the products
list is sorted by price using a stream and the sorted()
method with a comparator. The sorted products are then collected into a new list.
By mastering these advanced techniques, you can handle complex sorting scenarios with greater flexibility and control, ensuring that your Java applications can efficiently sort data according to your specific requirements.
5. Best Practices For Using Comparators
To ensure that your comparators are effective, efficient, and maintainable, it’s important to follow some best practices. These guidelines cover various aspects of comparator usage, from design principles to performance considerations.
5.1. Keep Comparators Simple And Focused
Comparators should be designed to perform a single, well-defined comparison. Avoid including complex logic or side effects within the compare()
method. A focused comparator is easier to understand, test, and maintain.
// Good: A simple comparator that compares products by price
public class ProductPriceComparator implements Comparator<Product> {
@Override
public int compare(Product p1, Product p2) {
return Double.compare(p1.getPrice(), p2.getPrice());
}
}
// Bad: A comparator with complex logic and side effects
public class ComplexProductComparator implements Comparator<Product> {
@Override
public int compare(Product p1, Product p2) {
// Complex logic to determine the comparison result
double priceDiff = p1.getPrice() - p2.getPrice();
if (priceDiff > 0) {
// Side effect: Update some external state
p1.setDiscountApplied(true);
return 1;
} else if (priceDiff < 0) {
return -1;
} else {
return 0;
}
}
}
In the “Good” example, the comparator simply compares products by price. In the “Bad” example, the comparator includes complex logic and a side effect (updating the discount applied status), making it harder to understand and maintain.
5.2. Use Lambda Expressions For Conciseness
Java 8 introduced lambda expressions, which provide a more concise way to define comparators. Use lambda expressions to simplify your code and make it more readable.
// Before: Using an anonymous class
Comparator<Product> priceComparator = new Comparator<Product>() {
@Override
public int compare(Product p1, Product p2) {
return Double.compare(p1.getPrice(), p2.getPrice());
}
};
// After: Using a lambda expression
Comparator<Product> priceComparator = (p1, p2) -> Double.compare(p1.getPrice(), p2.getPrice());
The lambda expression (p1, p2) -> Double.compare(p1.getPrice(), p2.getPrice())
is more concise and easier to read than the anonymous class.
5.3. Handle Null Values Gracefully
Always handle null values in your comparators to avoid NullPointerException
errors. Use the Comparator.nullsFirst()
or Comparator.nullsLast()
methods to specify how null values should be treated.
import java.util.Comparator;
public class Product {
private String name;
private Double price;
public Product(String name, Double price) {
this.name = name;
this.price = price;
}
public String getName() { return name; }
public Double getPrice() { return price; }
@Override
public String toString() {
return name + " (" + price + ")";
}
public static void main(String[] args) {
// Comparator that handles null prices
Comparator<Product> priceComparator = Comparator.comparing(Product::getPrice, Comparator.nullsFirst(Comparator.naturalOrder()));
}
}
In this example, Comparator.nullsFirst(Comparator.naturalOrder())
ensures that products with null prices are placed at the beginning of the sorted list.
5.4. Prefer Method References Over Lambda Expressions When Possible
When using lambda expressions, prefer method references when they make the code more readable and concise. Method references are especially useful when you are simply calling an existing method on the objects being compared.
// Before: Using a lambda expression
Comparator<Product> nameComparator = (p1, p2) -> p1.getName().compareTo(p2.getName());
// After: Using a method reference
Comparator<Product> nameComparator = Comparator.comparing(Product::getName);
The method reference Product::getName
is more concise and easier to understand than the lambda expression.
5.5. Use ThenComparing()
For Multiple Criteria
When sorting by multiple criteria, use the thenComparing()
method to chain comparators. This makes your code more readable and maintainable.
import java.util.Comparator;
public class Employee {
private String name;
private double salary;
private int age;
public Employee(String name, double salary, int age) {
this.name = name;
this.salary = salary;
this.age = age;
}
public String getName() { return name; }
public double getSalary() { return salary; }
public int getAge() { return age; }
@Override
public String toString() {
return name + " (" + salary + ", " + age + ")";
}
public static void main(String[] args) {
// Sort by salary, then by age, then by name
Comparator<Employee> employeeComparator = Comparator.comparing(Employee::getSalary)
.thenComparing(Employee::getAge)
.thenComparing(Employee::getName);
}
}
In this example, the employeeComparator
sorts employees by salary, then by age, and finally by name, using the thenComparing()
method.
5.6. Ensure Comparators Are Consistent With Equals
Comparators should be consistent with the equals()
method of the objects being compared. This means that if compare(a, b)
returns 0, then a.equals(b)
should also return true. While this is not strictly enforced by the Comparator
interface, it’s a good practice to ensure that your comparators provide a consistent and predictable ordering.
import java.util.Objects;
import java.util.Comparator;
public class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() { return name; }
public int getAge() { return age; }
@Override
public String toString() {
return name + " (" + age + ")";
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass