Wednesday, June 30, 2021

Java Tutorial: Generics

Chapters

Java Generics

Generics were introduced in JDK5 and allow object types to be a parameter type to methods, class and interface. Generic types are different from method parameter-list.

Generics ensure type safety. During runtime, object types can be problematic due to their dynamic nature. For example, objects can be casted into their relative objects. Sometimes, object casting can be messy and error prone.

By using generics, we can reduce type errors during runtime and detect some of those errors at compile-time. Also, using generics may eliminate the use of explicit typecasting.

To declare generic types in a method,class or interface; We will use the diamond operator"<>" and put generic parameter names in there. Java has naming convention for generic parameter names:
  • E - Element (mostly used in Java Collections Framework)
  • K - Key
  • N - Number
  • T - Type
  • V - Value
  • S,U,V etc. - 2nd, 3rd, 4th types
Example: class ClassA<T>{ //codes }

If methods have parameter and argument types, then generics have type parameters and arguments.

Type arguments are actual types that can be passed to parameter types and they're used to create parameterized type. Parameterized type is a declaration of generic types with type arguments.
e.g. ClassA<String> classA;

Type parameters are used to declare parameter names or type variables in methods,class or interfaces.
e.g. class ClassA<T>{ //codes }

Generic Methods

We can make a method generic by putting the diamond operator before the method return type. We still need to follow method overloading and overriding rules if we want to overload or override generic methods. The scope of the declared parameter types is limited to the method block.
public class SampleClass{
  
  //Diamond operator with one parameter type
  <T> void displayMessage(T message){
    System.out.println(message);
  }
  
  //Diamond operator with multiple parameter types
  //parameter types must be separated by comma
  <T,U> void displayMessage(T message, U greetings){
    Greetings greet = null;
    
    //We're going to use explicit cast here for now.
    //We can use bounded type parameter here to
    //replace this explicit
    //We learn bounded type parameter later
    //later
    if(greetings instanceof Greetings)
      greet = (Greetings)greetings;
    else throw new NullPointerException("greet is null");
    
    System.out.println(greet.getGreetings());
    System.out.println(message);
  }
  
  //method with generic return type
  <T> T getMessage(T message){
    return message;
  }
  
  public static void main(String[]args){
    
    SampleClass sc = new SampleClass();
    
    //calling generic method with type witness.
    //type witness is a type argument specified
    //in a generic method/constructor call
    sc.<String>displayMessage("Hello Everyone!");
    
    //calling generic method using type inference which
    //we will discuss later.
    sc.displayMessage("Have a good day!",
                      new Greetings("Hi!"));
    String msg = new Greetings("Hey!").getGreetings();
    
    if(msg instanceof String)
      msg = sc.getMessage(msg);
      
    System.out.println("The message is: " + msg);
  }
}

class Greetings{
  private String greet;
  
  public Greetings(String greet){
    this.greet = greet;
  }
  
  public String getGreetings(){
    return greet;
  }
}
First off, we declare generic type parameters by using the diamond operator then, we can use those type parameters as type in method's parameter-list or as method's return type.

The generic types declaration in the example above is called unbounded type. During type erasure, the type of these unbounded type parameters is going to be the The Object class which is the parent class of all classes. We will discuss type erasure later.

We also used type inference in the example above. To put it simply, type inference is a way of omitting type arguments. We will discuss type inference later.

Generic Class and Interface

Class or interface can be generic too by putting the diamond operator next to the class or interface name. The scope of the declared parameter types is limited to the class or interface block.
public class SampleClass{
  
  public static void main(String[]args){
    
    //type arguments or parameterized type
    //when declaring a generic class
    Message<String> msg = new Message<String>();
    
    //Omitting the type arguments at the right side using type
    //inference.
    MyFileOp<String,Integer> mfo = new MyFileOp<>(1);
    
    //Error: "a" is not an Integer. This is one of the type safety
    //features of generics.
    //MyFileOp<String,Integer> mfo = new MyFileOp<>("a");
    
    msg.displayMessage(mfo.checkFileName("Filename"));
    msg.displayMessage(mfo.checkFilePath("C:\\test\\Filename.txt"));
    msg.displayMessage(String.valueOf(mfo.getFileID()));
  }
}

//generic interface
interface FileOp<T>{

  T checkFileName(T fileName);
  T checkFilePath(T filePath);
  
}

//generic class that inherited a generic interface
class MyFileOp<T,U> implements FileOp<T>{
  //Generic variable
  private U fileId;
  
  MyFileOp(U fileId){
    this.fileId = fileId;
  }
  
  @Override
  public T checkFileName(T fileName){
    System.out.println("Checking filename...");
    System.out.println("File name verified!");
    return fileName;
  }
  
  @Override
  public T checkFilePath(T filePath){
    System.out.println("Checking filepath...");
    System.out.println("File path verified!");
    return filePath;
  }
  
  U getFileID(){
    return fileId;
  }
}

//generic class without inheritance
class Message<T>{

  void displayMessage(T message){
    System.out.println("Message: " + message);
  }
}
In the example above, once the parameter types are declared by using the diamond operator in the class or interface, we can use those parameter types as types of instance or local variables, return types of class' methods, etc.

If a generic class inherits another generic class or interface then, we should redeclare the superclass parameter types in the subclass definition like we did in FileOp interface and MyFileOp class in the example above.

In the example above, MyFileOp class inherits FileOp interface then, FileOp type parameter is redeclared in MyFileOp as type T of MyFileOp. So, type T of MyFileOp<T,U> is also the type parameter of FileOp<T>. This process is called subtyping.

Generic Constructor

We can declare a constructor as generic by using the diamond operator before the constructor name. The scope of the declared type parameters is limited to constructor block.
public class SampleClass{
  private Object obj1,obj2;
  
  <T> SampleClass(T obj1,T obj2){
    this.obj1 = obj1;
    this.obj2 = obj2;
  }
  
  boolean compareObjects(){
    
    if(obj1 == obj2)
      return true;
      
    return false;
  }
  
  public static void main(String[]args){
    
    String str1 = new String("My String");
    String str2 = str1;
    
    //generic constructor call with type witness.
    //type witness is a type argument specified
    //in a generic method/constructor call
    //SampleClass sc = new <String>SampleClass(str1,str2);
    
    //type inference in generic constructor call
    SampleClass sc = new SampleClass(str1,str2);
    
    if(sc.compareObjects())
      System.out.println("objects are the same");
    else
      System.out.println("objects are not the same");
  }
}
Type Arguments in Generics Inheritance

We can use type arguments in class, method and constructor definition to specify the type of their type parameters if a subclass(generic or not) inherits them. Constructors can't be inherited but can be called by subclass using the super() keyword.
public class SampleClass{

  public static void main(String[]args){
  
    ClassB<String> b = new ClassB<>(
                             "ClassA","ClassB",1);
    System.out.println("ClassB Variable");
    
    //This is valid since the parameters of
    //compreString() in ClassB behave like Object
    //So, Object or any type that has Object
    //as parent class is valid as arguments 
    //in this method
    System.out.println(b.compareString(1,2));
    System.out.println();
    
    //Invalid upcast 'cause ClassB<T>
    //superclass is ClassA<String>
    //ClassA<Integer> a = b;
    
    //valid upcast
    ClassA<String> a = b;
    System.out.println("ClassA Variable");
    
    //This is valid 'cause the parameter type
    //of this method in ClassA is unbounded
    //type parameters. The base type of
    //unbounded type parameters is Object type
    //So, Object or any type that has Object
    //as parent class is valid as arguments 
    //in this method
    //Note: We will discuss base type in type
    //erasure topic
    System.out.println(a.compareString(2,2));
  }
}

abstract class ClassA<T>{
  private T nameA;
  
  <S>ClassA(T nameA, S num){
    this.nameA = nameA;
    System.out.println("num: " + num.toString());
  }
  
  <U>boolean compareString(U o1, U o2){
    System.out.println("o1: " + o1.toString());
    System.out.println("o2: " + o2.toString());
    
    return o1.toString().equals(o2.toString());
  }
  
  void displayName(){
    System.out.println(nameA);
  }
}

//Type argument being used as a specific type to "T"
//of ClassA.
class ClassB<U> extends ClassA<String>{
  private U nameB;
  
  ClassB(String nameA, U nameB, Integer num){
    //"T" in ClassA is specified as String in ClassB
    //"T" in ClassA is used as type of the first 
    //parameter of ClassA's constructor. So, the
    //first argument in the argument list should be
    //a String. Try changing the type of nameA and
    //the compiler will throw a compile-time error
    //
    //Then, "S" in ClassA's constructor is specified as
    //Integer in ClassB
    //"S" in ClassA's constructor is used as type of the
    //constructor's second parameter
    //So, the second argument in the argument list should
    //be an Integer. Try changing the type(except for int
    //primitive 'cause it will be automatically wrapped
    //into Integer) of num and the compiler will throw a
    //compile-time error
    <Integer>super(nameA,num);
    this.nameB = nameB;
  }
  
  //"U" in compareString() in ClassA is specified as String in
  //ClassB
  //"U" in ClassA is used as type of the paremeter-list of
  //compareString() in ClassA. So, the parameter-list of the
  //overriding method should be String. Try changing the type
  //of the parameter-list of the overriding class and the 
  //compiler will throw a compile-time error.
  //
  //Though, The parameters here still behave as Object type
  //which is the base type of unbounded type parameter.
  @Override
  <String> boolean compareString(String o1, String o2){
    //Can't use concat() even o1 is a String type 'cause
    //the parameters in this method still behave as Object
    //type
    //String str = o1.concat(o2);
  
    System.out.println("o1: " + o1.toString());
    System.out.println("o2: " + o2.toString());
    
    return o1.toString().equals(o2.toString());
  }
  
  @Override
  void displayName(){
    System.out.println(nameB);
  }
}
Note: Better use type arguments in method,class and constructor definition during inheritance. Using type arguments in a parent class without children, parent class constructor and overriden method doesn't make sense.
//This syntax is valid but the use of 
//type arguments here doesn't make sense
class ClassA<String>{
  Object obj;
}

Type Inference

Type inference is the ability of the compiler to determine the corresponding type argument declaration in a statement. We already encountered type inference in previous topics. For example, We can omit the type arguments in the instantiation statement and in the method call.
public class SampleClass{
  
  static <T> void displayMessage(T message){
    System.out.println("This is the message: " + message);
  }
  
  <T>SampleClass(T message){
    System.out.println(message);
  }
  
  public static void main(String[]args){
    //valid statement but can be shorten
    //ClassA<String> a = new ClassA<String>();
    
    //type inference in instantiation statement
    //This type inference may not work on java versions
    //prior to java SE7
    ClassA<String> a = new ClassA<>();
    
    //valid generic method call but can be shorten
    //SampleClass.<String>displayMessage("Message!");
    
    //type inference in generic method call
    SampleClass.displayMessage("Message!");
    
    //valid generic constructor call
    //new <String>SampleClass("Hey!");
    
    //type inference in generic constructor call
    new SampleClass("Hey!");
    
  }
}

class ClassA<T>{ /*codes*/ }
Unlike in the type inference of generic method and constructor call, we shouldn't remove the diamond operator in the type inference of the instantiation statement.
public class SampleClass{

  public static void main(String[]args){
  
    //this will throw a warning
    ClassA<String> a = new ClassA();
  }
}

class ClassA<T>{ /*codes*/ }
Type Inference of Generic Class and Constructor

We know that we can declare type parameters to methods,constructors and classes. What if a generic class has generic class in its block? Let's take a look at this example.
public class SampleClass{

  public static void main(String[]args){
    
    //valid type inference
    ClassA<Integer> a = new ClassA<>("Hey!",1);
    
    //This call will throw a warning related to
    //raw types
    //new ClassA("Hey!",1);
    
    //valid type inference.The parameters in ClassA's
    //constructor use unbounded generic types. The
    //base type of unbounded type parameters is Object.
    //So, any type that has Object as superclass
    //is a valid argument
    //new ClassA<>("Hey!",1);
    
    //valid type inference. Generic class doesn't
    //use type inference here but the generic
    //constructor does. 
    //new ClassA<Integer>("Hey!",1);
    
    //invalid type inference. Can't use type inference in
    //generic class because generic constuctor doesn't
    //use type inference
    //new <String>ClassA<>("Hey",1);
    
    //valid type inference.
    //new <String>ClassA<Integer>("Hey",1);
  }
}

//generic class
class ClassA<T>{
  
  //generic constructor
  <U>ClassA(U message,T num){
    System.out.println(message + " " + num);
  }
  
  /*
  //generic constructor/method
  //in a generic class can have
  //the same type parameter name
  //but it will make our generic 
  //code confusing and restrictive
  <T>ClassA(T message,Integer num){
    System.out.println(message + " " + num);
  }
  */
  
}
Target Typing in Type Inference

Target type is a type in an expression that is expected by the compiler. In typecasting(Objects) we use target typing to explicitly cast one object to another. e.g. String str = (String)obj1;
The type "(String)" is the target type.

In generics, we can use target typing to assign a generic return type of a method to a generic variable.
public class SampleClass{
  
  public static void main(String[]args){
    
    ClassA<String> a1 = new ClassA<>("ClassA");
    ClassA<String> a2 = new ClassA<>("ClasssA");
    
    //Try changing a1 and a2 to this
    //ClassA<Integer> a1 = new ClassA<>(1);
    //ClassA<Integer> a2 = new ClassA<>(2);
    
    System.out.println("Is a2 name longer than a1?");
    
    //The "<String>" in "ClassA<String>" is
    //the target type. The target type infers that 
    //compareNameLength() "<S>" type should be
    //<String> type. Try changing the type
    //arguments of a1 and a2 to Integer and this code
    //will throw a compile time error similar to this:
    //Inference variable S has incompatible equality
    //constraints
    ClassA<String> a3 = a1.compareNameLength(a2);
    
    if(a3 != null)
      System.out.println("Yes!");
    else System.out.println("No!");
  }
}

//Singleton Generic Class
class ClassA<T>{
  private T name;
  private ClassA<T> instance;
  
  ClassA(T name){
    this.name = name;
  }
  
  //<S> before ClassA<S> return type
  //is the declaration of type parameter
  //ClassA<S> is the return type
  //ClassA<S> o1 is the parameter type
  <S>ClassA<S> compareNameLength(ClassA<S> o1){
    if(o1.getName().toString().length() > 
       name.toString().length()) return o1;
    else return null;
  }
  
  T getName(){
    return name;
  }
  
}

Raw Types

Raw type is a class or interface type that doesn't declare type arguments. For new applications, raw types should be avoided. One of The reasons why raw types can still be used today is due to the backward compatibility with legacy codes prior to JDK 5.0. Lots of API classes prior to JDK 5.0 are non-generic.
Note: Class or interface type that is not generic is not a raw type. In other words, the type of class or interface that doesn't have type parameter is not a raw type.
public class SampleClass{

  public static void main(String[]args){
    //Declaring generic ClassA with
    //type arguments
    //ClassA<String> a1 = new ClassA<>("ClassA");
    
    //This declaration will throw a warning
    //regarding raw types 'cause we
    //we're trying to assign a raw type to
    //a generic variable
    //
    //The warning is similar to this:
    //Note: SampleClass.java uses unchecked or
    //unsafe operations
    //Note: Recompile with -Xlint:unchecked
    //for details
    ClassA<String> a2 = new ClassA("ClassA");
    
    //This however, won't throw a warning due to
    //backward compatibility with legacy code.
    //We can legally mix pre-generics(raw types)
    //and generics(parameterized type) code this way.
    //ClassA a2 = new ClassA<String>("ClassA");
    
    //This will also throw a warning regarding
    //raw types
    //InterfaceA<String> ia = new ClassB();
    
    //This declaration is also valid but not
    //recommended 'cause we're instantiating
    //a raw type of ClassA<T>
    //The type "T" in ClassA will be the
    //base type of unbounded type parameter
    //which is the Object type.
    //ClassA a3 = new ClassA();
    
  }
}

class ClassA<T>{
  private T name;
  
  ClassA(T name){
    this.name = name;
  }
  
  T getName(){
    return name;
  }
}

interface InterfaceA<T>{
}

class ClassB<T> implements InterfaceA<T>{
}
Note: "Unchecked or unsafe operations" in the compiler warning states that raw types may cause problematic type problems because compiler doesn't have enough information to perform type safety. One of the problematic problems that we're going to get when using raw types is the ClassCastException.
public class SampleClass{

  public static void main(String[]args){
  
    ClassA<String> a1 = new ClassA(10);
    //ClassCastException
    String str = a1.name;
  }
}

class ClassA{
  T name;

  CkassA(T name){
    this.name = name;
  }
}
The example above will compile just fine. However, when we run the compiled code, java will throw a run-time exception which is the ClassCastException.

If you're using a command prompt when compiling your java code then, you can type this command to reveal the warning that I mentioned earlier. This command assumes that the SampleClass.java is not in a package and the command prompt is pointing at the folder where SampleClass.java is located
javac SampleClass.java -Xlint:unchecked

Bounded Type Parameters

We can narrow down types that can be passed on type paramaters by creating a bounds by writing the type parameter, followed by extends keyword, followed by the upper bound.
Note: The use of extends keyword in this context is used to mean either "extends"(classes) or "implements"(interfaces)
public class SampleClass{

  public static void main(String[]args){
    
    //Integer is a subclass of Number so,
    //the type argument here is valid
    ArithmeticOperations<Integer> ao =
    new ArithmeticOperations<>();
    
    System.out.println(ao.addInt(2,3));
    System.out.println(ao.addDouble(2,3));
    
    //This won't work 'cause String is not
    //Number and not a subclass of Number
    //ArithmeticOperations<String> ao =
    //new ArithmeticOperations<>();
  }
}

//The type parameter "T" of this class has an
//upper bound which is the Number class. We
//use the extends keyword to bound "T" to
//Number class.
//If "T" is bounded to Number class then,
//"T" type parameter can only accept Number
//and its subclasses as type arguments
class ArithmeticOperations<T extends Number>{
  
  //intValue() returns int which is automatically
  //wrapped into Integer if int is assigned to
  //an Integer
  Integer addInt(T num1, T num2){
    return num1.intValue() + num2.intValue();
  }
  
  //doubleValue() returns double which is automatically
  //wrapped into Double if double is assigned to
  //a Double
  Double addDouble(T num1, T num2){
    return num1.doubleValue() + num2.doubleValue();
  }
}
In the example above, we see that we can use new methods like intValue() and doubleValue(). That's because the base type of "T" is now the Number type not the Object type anymore. In previous topics, we can only use methods provided by the The Object class like the toString() method.

In this example,we can use methods that the Number class provided like intValue(), doubleValue(), etc.

Multiple Bounds

A type variable in this bounds is a subclass of all bounds that are listed in the diamond operator separated by "&".
public class SampleClass{

  public static void main(String[]args){
    
    //Valid type argument 'cause ClassD
    //is a subclass of ClassC,InterfaceA
    //and InterfaceB
    ClassE<ClassD> e1 = new ClassE<>(new ClassD());
    
    //Invalid type argument 'cause ClassA is a
    //subclass of InterfaceA but not a subclass
    //of InterfaceB and ClassC
    //ClassE<ClassA> e2 = new ClassE<>(new ClassA());
    
    //Invalid type argument 'cause specified bounds are
    //exclusive
    //ClassE<ClassC> e2 = new ClassE<>(new ClassC());
  }
}

interface InterfaceA{
}

class ClassA implements InterfaceA{
}

interface InterfaceB{
}

class ClassB implements InterfaceB{
}

class ClassC{
}

class ClassD extends ClassC implements InterfaceA,
                                       InterfaceB{
}

//When implementing this kind of bounds, class must
//come first on the list before interfaces. Try putting
//ClassC in the last entry of the list and the compiler
//will throw a compiler-time error
//class ClassE<T extends InterfaceA & InterfaceB & ClassC>{/**/}
class ClassE<T extends ClassC & InterfaceA & InterfaceB>{
  T instance;
  
  ClassE(T instance){
    this.instance = instance;
  }
}
Using comma(,) to create multiple bounds is legal. However, the usage of this kind of multiple bounds doesn't make any sense.
public class SampleClass{

  public static void main(String[]args){
    
    //First off, we are required to put
    //three type arguments here to match
    //the type parameters in ClassE
    //
    //second, we can change the second and
    //third type arguments even those
    //replacements are not related to the
    //type parameters
    //
    //The only important part here is the
    //first argument. Change the first
    //type argument and the compiler will
    //throw a compile-time error
    //
    //The compiler won't throw a compile-time
    //error if we change the second and third
    //type arguments
    //
    //If we change the object type that is passed
    //to constructor to the type of second and
    //third type arguments then, the compiler
    //will throw a compile-time exception.
    //
    //The compiler won't throw a compile-time
    //error if we pass an object with a type
    //of the first argument or subclasses of
    //the first argument
    ClassE<ClassC,String,Integer> e =
    new ClassE<>(new ClassC());
  }
}

interface InterfaceA{
}

class ClassA implements InterfaceA{
}

interface InterfaceB{
}

class ClassB implements InterfaceB{
}

class ClassC{
}

class ClassD extends ClassC implements InterfaceA,
                                       InterfaceB{
}

//These type of multiple bounds is legal but 
//it doesn't make sense
class ClassE<T extends ClassC,InterfaceA,InterfaceB>{
  T instance;
  
  ClassE(T instance){
    this.instance = instance;
  }
}

//In my opinion, This multiple bounds in this example is simply
//equal to a bounded type parameter
/*
class ClassE<T extends ClassC>{
  T instance;
  
  ClassE(T instance){
    this.instance = instance;
  }
}
*/
Generic Wildcard

In generics, the "?" symbol is called the wildcard. This wildcard represents an unknown type and it can be used as a generic parameter, field, local variable and a return type(Though, it's a good practice to have specific return type). We never use the wildcard as a type argument for generic method call, generic class instantiation or type parameter of generic class.

Unbounded Wildcard

We can use unbounded wildcard if we want a type that doesn't rely on declared type parameter. Let's take a look at this example.
public class SampleClass{
  
  //No need to declare a paremeter type 'cause
  //we're using unbounded wildcard
  static void displayString(ClassA<?> obj){
    System.out.println(obj.toString());
  }
  
  /*
  The method above is similar to this
  method. Remember, the base type of 
  unbounded wildcard is Object
  static void displayString(ClassA<Object> obj){
    System.out.println(obj.toString());
  }
  */
  
  public static void main(String[]args){
    
    SampleClass.displayString(new ClassA<String>("String"));
    SampleClass.displayString(new ClassA<Integer>(200));
  }
}

class ClassA<T>{
  private T name;
  
  ClassA(T name){
    this.name = name;
  }
  
  @Override
  public String toString(){
    return name.toString();
  }
}
So, what's the difference betweenClassA<?> and ClassA<Object>?
ClassA<?> accepts any type that is a subclass of Object whereas ClassA<Object> only accepts Object instance. Take a look at this example.
public class SampleClass{
  
  static void displayString(ClassA<Object> obj){
    System.out.println(obj.toString());
  }
  
  public static void main(String[]args){
    
    //valid argument
    SampleClass.displayString(new ClassA<Object>( new Object() ));
    //invalid argument
    //SampleClass.displayString(new ClassA<Integer>(200));
  }
}

class ClassA<T>{
  private T name;
  
  ClassA(T name){
    this.name = name;
  }
  
  @Override
  public String toString(){
    return name.toString();
  }
}
Upper Bounded Wildcard

Just like type parameter, we can set bounds to the wildcard. To set an upper bound, use the "?" symbol, followed by the extends keyword, followed by the upper bound.
Upper bound restricts the unknown type to be the upper bound or the subclasses of the upper bound.
Note: The use of extends keyword in this context is used to mean either "extends"(classes) or "implements"(interfaces)
import java.util.List;
import java.util.Arrays;
public class SampleClass{

  static String concat(List<? extends CharSequence> lt){
    
    String str = "";
    for(CharSequence sq : lt)
      str = str.concat(sq.toString() + " ");
      
    return str;
  }
  
  public static void main(String[]args){
    List<String> lt = Arrays.asList("Java","Tutorial:","Generics");
    System.out.println("Combined Strings: " + SampleClass.concat(lt));
  }
}
In the example above, we use CharSequence interface as the upper bound. Thus, the parameter "lt" will only accept CharSequence and subclasses of CharSequence.

Note that we can apply upper bound to a type-parameter.
Example:
public static <T extends String> String message(T msg1, T msg2);

Lower Bounded Wildcard

To set a lower bound for the wildcard, we use the wildcard symbol(?), followed by the super keyword, followed by the lower bound. The lower bound could be a class or interface. Lower bound restricts the unknown type to be the lower bound or the superclasses of the lower bound.
import java.util.List;
import java.util.Arrays;
public class SampleClass{
  
  static void displayString(List<? super String> lt){
    
    for(Object o : lt)
      System.out.println(o.toString());
  }
  
  public static void main(String[]args){
    List<CharSequence> lt = Arrays.asList("Lower","Bounded",
                                                "Wildcard");
    SampleClass.displayString(lt);
  }
}
In the example above, we use String class as the lower bound. Thus, the parameter "lt" will only accept String and superclasses of String.

Wildcard Capture and Helper Methods

Sometimes, during evaluation, a particular type of a wildcard is inferred by the compiler. This phenomenon is called the wildcard capture. Let's take a look at this example.
import java.util.List;
public class SampleClass{
  
  static void swapNumber(List<? extends Number> lt,
                         int i1,int i2){
    //try changing o1 to String and you will receive a
    //compile-time error similar to this: CAP#1 can't be
    //converted to String
    //CAP#1 is the type of the wildcard that is inferred
    //String o1 = lt.get(i1);
    
    Number o1 = lt.get(i1);
    Number o2 = lt.get(i2);
    
    //Error: Number cannot be converted to CAP#1
    //....
    //Where CAP#1 is a fresh type-variable
    //CAP#1 extends Number from a capture of ? extends Number
    lt.set(i1,o2);
    lt.set(i2,o1);
    
  }
  
  public static void main(String[]args){
  }
}
In the example above, we can't use the set() method of lt 'cause of type error. Seems like the compiler is unsure about the type that we're putting in the set() method. It's possible to fix the error in the example by using type inference by creating a private helper method which captures the wildcard.
import java.util.List;
import java.util.Arrays;
public class SampleClass{
  
  static void swapNumber(List<? extends Number> lt,
                         int i1,int i2){
    
    //We use type inference here to capture 
    //CAP#1 and pass it on "T"
    swapNumberHelper(lt,i1,i2);
  }
  
  //We can also copy the bounds in swapNumber().
  //Though, a plain "T" is enough
  //private static <T extends Number>void
  //swapNumberHelper(List<T> lt,int i1,int i2){ /**/ }
  
  //Helper method
  private static <T>void swapNumberHelper(
                               List<T> lt,
                               int i1,int i2){
    T o1 = lt.get(i1);
    T o2 = lt.get(i2);
    lt.set(i1,o2);
    lt.set(i2,o1); 
  } 
  
  public static void main(String[]args){
    List<Integer> lt = Arrays.asList(2,4);
    
    System.out.println("First Element: " + lt.get(0));
    System.out.println("Last Element: " + lt.get(1));
    System.out.println("Swapping...");
    SampleClass.swapNumber(lt,0,1);
    System.out.println("First Element: " + lt.get(0));
    System.out.println("Last Element: " + lt.get(1));
  }
}
Not all wildcard capture errors can be fixed. Take a look at this example.
import java.util.List;
import java.util.Arrays;
public class SampleClass{
  
  static void swapNumber(List<? extends Number> lt1,
                         List<? extends Number> lt2,
                         int i1, int i2){
   swapNumberHelper(lt1,lt2,i1,i2);             

  }
  
  private static <T>void swapNumberHelper(
                               List<T> lt1,
                               List<T> lt2,
                               int i1, int i2){
    T o1 = lt1.get(i1);
    T o2 = lt2.get(i2);
    
    lt1.set(i1,o2);
    lt2.set(i2,o1);
  }
  
  public static void main(String[]args){
    List<Integer> lt1 = Arrays.asList(2,4);
	List<Float> lt2 = Array.asList(2.5f,3.4f);
	SampleClass.swapNumber(lt1,lt2,0,1);
  }
}
The wildcard capture error above can't be fixed because there are two wildcard capture types which are CAP#1 and CAP#2. No matter what we do in this example, we can't swap Objects between two lists with different wildcard capture types.

Sometimes, Wilcard capture error may happen due to the presumption of a type of the wildcard.
import java.util.List;
import java.util.Arrays;
public class SampleClass{
  
  static void displayString(List<? extends CharSequence> lt){
    
    //we can't presume that the type of lt is
    //a String even we specify String type
    //argument on the list in main()
    //this is unsafe
    for(String s : lt)
      System.out.println(s);
      
    //Use this instead since the upper
    //bound of lt is CharSequence
    //This presumption is safe
    //for(CharSequence s : lt)
    //  System.out.println(s.toString());
  }
  
  public static void main(String[]args){
    List<String> lt = Arrays.asList("A","B","C");
    SampleClass.displayString(lt);
  }
}
Generic Class/Interface in Type Parameter/Argument

A type parameter can have generic class or interface as types.
import java.util.List;
import java.util.Arrays;
public class SampleClass{
  
  //Generic class in type parameter. We use
  //type argument to specify the generic type
  //of ClassA. It means, "T" declared in 
  //getInstance() only accepts ClassA<String>
  static <T extends ClassA<String>>
  void getInstance(List<T> a){
    for(ClassA ca : a)
      System.out.println(ca.toString());
  }
  
  //Generic class in type parameter. "T"
  //before the extends is a parameter type
  //with Comparable<T> as upper bound.
  //It means, the parameter type only accepts
  //classes/interfaces that implements/extends
  //the Comparable<T> interface
  static <T extends Comparable<T>>
  void compare(T o1, T o2){
    if(o1.compareTo(o2) ==
       o2.compareTo(o1))
    System.out.println(o1.toString() + 
                       " is equal to " +
                       o2.toString());
    else
    System.out.println(o1.toString() + 
                       " is not equal to " +
                       o2.toString());
  }
  
  public static void main(String[]args){
    //Generic class as type argument
    List<ClassA<String>> lt =
    Arrays.asList(new ClassA<>("a1"),
                  new ClassA<>("a2"),
                  new ClassA<>("a3"));
    SampleClass.getInstance(lt);
    
    //Integer and String implement Comparable<T>
    //That' why they're valid arguments
    SampleClass.compare("abcd","ABCD");
    SampleClass.compare(33,100);
  }
}

class ClassA<T>{
  private T name;
  
  ClassA(T name){
    this.name = name;
  }
  
  @Override
  public String toString(){
    return name.toString();
  }
}
Generics Subtyping

In non-generic types, we can assign a type to another type if the types are compatible. However, this doesn't apply to generic types.
public class SampleClass{

  public static void main(String[]args){
    //valid
    CharSequence sc = new String("String");
    
    ClassA<CharSequence> a1 = null;
    ClassA<String> a2 = new ClassA<>();
    //invalid 'cause ClassA<CharSequence>
    //is not related to ClassA<String>
    a1 = a2;
  }
}

class ClassA<T>{
}
We can subtype generic classes or interfaces by using inheritance. However, we shouldn't forget to include type parameter declaration of parent class in a subclass.
public class SampleClass{

  public static void main(String[]args){
    
    //valid: ClassB<String> is a
    //subclass of InterfaceA<String>
    InterfaceA<String> ia = new ClassB<>();
    
    //valid: ClassB is a subclass of ClassA
    ClassA<Integer> a1 = new ClassB<>();
  }
}

interface InterfaceA<T>{
}
//implements interface
class ClassA<T> implements InterfaceA<T>{
}

//extends generic class
class ClassB<U> extends ClassA<U>{
}
If we don't include type parameter declaration of parent class in a subclass during inheritance, then the compiler will throw an "unchecked conversion" warning everytime we instantiate that subclass. "Unchecked conversion" will appear if we assign a raw type to a generic type.
public class SampleClass{

  public static void main(String[]args){
    
    //This will throw an "unchecked conversion" warning
    InterfaceA<String> ia = new ClassB<>();
    
    //This will throw an "unchecked conversion" warning
    //ClassA<Integer> a1 = new ClassB<>();
  }
}

interface InterfaceA<T>{
}
//implements interface
class ClassA<T> implements InterfaceA<T>{
}

//extends generic class. Type parameter of ClassA is
//omitted
class ClassB<U> extends ClassA{
}
Because we omitted the type parameter of ClassA in ClassB, ClassB inherited the raw type version of ClassA. So, ClassB is considered as raw type because it inherited the raw type version of ClassA.

If we want to relate a generic type to another generic type then, we will use the generic wildcard("?")
public class SampleClass{

  public static void main(String[]args){
    
    //This type inference is invalid if used with
    //wildcard(?)
    //ClassA<?> a1 = new ClassA<>("a1");
    
    ClassA<String> a1 = new ClassA<String>("a1");
    ClassA<Integer> a2 = new ClassA<Integer>(100);
    //Subtyping using wildcard(?)
    ClassA<?> a3 = a1;
    System.out.println(a3.getString());
    a3 = a2;
    System.out.println(a3.getString());
  }
}

class ClassA<T>{
  private T string;
  
  ClassA(T string){
    this.string = string;
  }
  
  String getString(){
    return string.toString();
  }
}
Type Erasure

Type erasure is the process of removing type information related to generics and other generic elements to ensure that no new classes are created for parameterized types. Because there are no new classes are created, generics incur no runtime overhead.
These are the Type Erasure procedures according to The Java™ Tutorials
  • Replace all type parameters in generic types with their bounds or Object if the type parameters are unbounded. The produced bytecode, therefore, contains only ordinary classes, interfaces, and methods.

  • Insert type casts if necessary to preserve type safety.

  • Generate bridge methods to preserve polymorphism in extended generic types.(This is not applicable in java11 and above)

Let's create an example to demonstrate the first and second procedures of type erasure. This is our sample code structure before erasure
public class SampleClass{
  
  static void displayName(ClassA<?> a){
    System.out.println(a.getName());
  }
  
  static <T>boolean compareNames(T n1, T n2){
    return n1.toString().equals(n2.toString());
  }
  
  public static void main(String[]args){
    
    ClassA<String> a1 = new ClassA<>("a1");
    ClassA<String> a2 = new ClassA<>("a2");
    String str = a1.getName();
    System.out.println(str);
    System.out.println(SampleClass.compareNames(a1,a2));
    SampleClass.displayName(a2);
  }
}

class ClassA<T extends CharSequence>{
  private T name;
  
  ClassA(T name){
    this.name = name;
  }
   
  T getName(){ return name; }
  
  void setName(T name){
    this.name = name;
  }
}
This is our sample code structure after type erasure.
public class SampleClass{
  
  //wildcard has been removed
  static void displayName(ClassA a){
    System.out.println(a.getName());
  }
  
  //Type parameter has been removed
  static boolean compareNames(Object n1, Object n2){
    return n1.toString().equals(n2.toString());
  }
  
  public static void main(String[]args){
    
    //Diamond operators and type arguments have been
    //removed
    ClassA a1 = new ClassA("a1");
    ClassA a2 = new ClassA("a2");
    
    //Implicit addition of explicit cast
    String str = (String)a1.getName();
    
    System.out.println(str);
    System.out.println(SampleClass.compareNames(a1,a2));
    SampleClass.displayName(a2);
  }
}

//Type parameter has been removed
class ClassA{
  private CharSequence name;
  
  ClassA(CharSequence name){
    this.name = name;
  }
   
  CharSequence getName(){ return name; }
  
  void setName(CharSequence name){
    this.name = name;
  }
}
In the example above, the diamond operators with type parameter,arguments,wildcard and blank diamond operators have been removed. Then, the types of instance variables, local variables and return types that use type parameters are going to be the first bound or I would to call it "base type", if you will.

If a type parameter is unbounded, then the first bound of that type parameter is the Object class. Otherwise, the first bound of that type parameter is the upper bound.

Now we know that generics only exist at compile time. Once all type checks are complete and the compilation process is done, generics will be removed.

Even we know what the type "T" is going to be after erasure. We still can't assign an actual type to "T".
public class SampleClass{

  public static void main(String[]args){
  }
}

class ClassA<T>{
  private T name;
  //error
  //private T num = 10;
  //error
  //private T obj = new Object();
  Object o = null;
  
  ClassA(T name){
    //valid
    this.name = name;
    //We can assign T to its
    //first bound or base type
    o = name;
  }
  
  T getName(){
    //error
    //return name.toString();
    return name;
  }

}
Even we know that the "T" in the example above is going to be Object type, we still can't assign actual types to "T" even those types can be implicitly upcasted or explicitly downcasted.

Bridge Method

Note: Starting java11, bridge methods are not generated anymore. If you're using java version prior to java11 then this topic may still be helpful.

Bridge method links nested classes/interfaces, Thus making nested classes have unrestricted access to their public and non-public members. In this topic, bridge method is created to link a parameterized parent class/interface to its subclass. Bridge method is one of the procedures of type erasure.
public class SampleClass{

  public static void main(String[]args){
    ClassA a1 = new ClassB(2);
	try{ a1.setObj("Name2"); }
	catch(Exception e){
		e.printStackTrace();
	}
	
  }
}

class ClassA<T>{
  private T obj;
  
  ClassA(T obj){
    this.obj = obj;
  }
  
  void setObj(T obj){
    System.out.println("setObj() in ClassA");
    this.obj = obj;
  }
}

class ClassB extends ClassA<Integer>{

  ClassB(Integer obj){
    super(obj);
  }
  
  @Override
  void setObj(Integer obj){
	super.setObj(obj);
  }
}
The code will compile but we will get a ClassCastException. Let's apply type erasure to our code above to see the bridge method.
public class SampleClass{

  public static void main(String[]args){
    ClassA a1 = new ClassB(2);
	try{ a1.setObj("Name2"); }
	catch(Exception e){
		e.printStackTrace();
	}
	
  }
}

class ClassA{
  private Object obj;
  
  ClassA(Object obj){
    this.obj = obj;
  }
  
  public void setObj(Object obj){
    System.out.println("setObj() in ClassA");
    this.obj = obj;
  }
}

class ClassB extends ClassA{

  ClassB(Integer obj){
    super(obj);
  }
  
  //This method is the bridge method.
  //This method is compiler-generated
  //Note: Starting java11, synthetic
  //method or bridge method is not
  //generated anymore
  public void setObj(Object obj){
    //This is the code that throws
    //ClassCastException
    //String can't be converted to
    //Integer
    setObj((Integer)obj);
  }
  
  //This overriding process
  //is a failure because setObj()
  //in this class and setObj()
  //in ClassA have differrent
  //method signatures
  //
  //Although, In newer versions of
  //java, this overriding method
  //won't throw a compilation error
  @Override
  public void setObj(Integer obj){
	super.setObj(obj);
  }
}
Reifiable and Non-Reifiable Types

Refiable types are types, which type information is available at run time. These types are raw types, non-generic types, primitives and types that use unbounded wildcard.

Non-Reifiable types are types, which type information is not completely available at run time due to type erasure. Generic types, except for unbounded wildcard, are non-reifiable types.
For example, List<String> and List<Integer>; at runtime, these types are indistinguishable from each other because, when the generic information of these types is removed, both of them will be a normal List object.

You might ask: Why unbounded wildcard<?> is reifiable?
<?> is reifiable 'cause no type information will be removed if <?> is removed due to type erasure.
Take a look at this example.
import java.util.List;
import java.util.Arrays;
public class SampleClass{
  
  //Before erasure
  <T>void meth1(T type){
  }
  
  //After erasure
  //"T" is gone we can't use "T"
  //type information at runtime
  /*
    void meth1(Object type){
  }
  */
  
  //Before erasure
  void meth2(List<? extends Number> lt){
    
  }
  
  //After erasure
  //<? extends Number> is gone
  //we can't use the upper bound information
  //at runtime
  /*
  void meth2(List lt){
    
  }
  */
  
  //Before erasure
  void meth3(List<?> lt){
    
  }
  
  //After erasure
  //<?> is gone
  //Though, <?> doesn't
  //have type information
  //that may be usable at runtime
  //So, this method is pretty much
  //unaffected by erasure
  /*
  void meth3(List lt){
    
  }
  */
  
  public static void main(String[]args){
    //Before erasure
    List<Integer> lt = Arrays.asList(1,2);
    
    //After erasure
    //<Integer> is gone 
    //and can't be used at runtime
    //List lt = Arrays.asList(1,2);
  }
}


Heap Pollution

Heap pollution is a situation where a variable with parameterized type refers to an object that doesn't have that parameterized type. Heap pollution occurs in some situations like mixing raw types and parameterized type, etc. Our program may throw unexpected exceptions due to heap pollution.

Variable Arguments(varargs) is a hotspot for heap pollution. When we use generic varargs, java will throw a warning regarding heap pollution.
public class SampleClass{
  
  static <T>void displayString(T... elem){
    for(Object o : elem)
       System.out.println(o.toString());
  }
  
  public static void main(String[]args){
  
    SampleClass.displayString("A",1,4.5f,"C");
  }
}
One of the reasons why varargs is prone to heap pollution is due to the fact that varargs is translated to arrays and arrays of parametertized types are not allowable in java; even we can use type parameter as type of arrays. This means that arrays elements can't be generic. That's why the compiler can't ensure type safety on arrays.

So, T... is equivalent to T[] and after type erasure, the array will be like this: Object[]

This example demonstrates an exception thrown using varargs.
import java.util.List;
import java.util.Arrays;
public class SampleClass{
  
  static void listArr(List<String>... lt){
     //valid 'cause Object is a superclass
     //of List. So, Object[] is compatible with
     //List[]
     Object[] obj = lt;
     
     obj[0] = Arrays.asList(new StringBuilder("Builder"));
     
     //This will throw a ClassCastException, since lt[]
     //reference is assigned to obj[] and obj[] first element
     //is changed to StringBuilder.
     String s = lt[0].get(0); 
  }
  
  public static void main(String[]args){
    SampleClass.listArr(Arrays.asList("String"),
                        Arrays.asList("Builder"));
  }
}
If you're confident that the the generic varargs that you're using is safe to use then, we can suppress the warning regarding generic varargs. To suppress the warning, we will use the @SafeVarargs or @SuppressedWarnings annotation.
public class SampleClass{
  
  //We can use SuppressedWarnings but
  //@SafeVarargs is more preferrable 
  //when suppressing varargs related
  //warning
  //@SuppressWarnings({"unchecked", "varargs"})
  @SafeVarargs
  static <T>void displayString(T... elem){
    for(Object o : elem)
       System.out.println(o.toString());
  }
  
  public static void main(String[]args){
  
    SampleClass.displayString("A",1,4.5f,"C");
  }
}
Generics Restrictions

If we want to use java generics effectively, we need to know some of its restrictions.

1.) Primitive types like int,char, etc. can't be used as type arguments.
import java.util.List;
import java.util.Arrays;
public class SampleClass{
  
  public static void main(String[]args){
    //invalid
    List<int> lt = Arrays.asList(1,2,3);
    //valid
    //List<Integer> lt = Arrays.asList(1,2,3);
  }
}
2.) Type parameters can't be instantiated.
public class SampleClass{
  
  static <T>void displayName(T name){
    //invalid
    T localName = new T();
    
  }
  
  public static void main(String[]args){
  }
}
3.) Static fields whose types are type parameters can't be declared. Type parameters declared in class/interface definition are non-static in order to prevent conflict between type parameters from different classes.
//This sample code won't compile but
//let's assume that this code is valid
public class SampleClass<T>{
  //This invalid in real life
  static T name;
  
  public static void main(String[]args){
    //since name variable is static. It's being shared
    //to every instance of SampleClass. If we instantiate
    //SampleClass multiple times then, the type of name
    //variable is going to be ambiguious
    //In this example, we don't know if the type of the name
    //variable is a String or Integer
    SampleClass<String> sc1 = new SampleClass<>();
    SampleClass<Integer> sc1 = new SampleClass<>();
  }
}
4.) Type parameters declared in class/interface can't be used in nested static blocks.
public class SampleClass<T>{
  //valid
  T tName;
  
  static class StaticClass<U>{
    //invalid
    //T staticName;
    //valid
    U uName;
    
    //invalid parameter
    /*
    static void meth(U param){
    }
    */
  }
  
  class MyClass{
    //valid
    T className;
  }
  
  //1st parameter is invalid
  /*
  static <S>void myMeth(T param1,S param2){
  }
  */
  
  //all parameters are valid
  <Y>void anotherMeth(T param1,Y param2){
  }
  
  public static void main(String[]args){
  }
}
5.)We can't use typecasting or instanceof with parameterized types.
import java.util.List;
import java.util.LinkedList;
import java.util.Arrays;
public class SampleClass{
  
  static <E>void displayList(List<E> lt){
    //invalid: instanceof can't be used with 
    //parameterized type
    /*
    if(lt instanceof LinkedList<String>)
      System.out.println("invalid");
    */
  }
  
  public static void main(String[]args){
    //List<String> lta = Arrays.asList("A","B");
    //Invalid: can't do upcast
    //List<CharSequence> ltb = lta;
    
    //This typecast is valid 'cause unbounded
    //wildcard is reifiable type
    List<?> lt1 = Arrays.asList("A","B");
    List<?> lt2 = lt1;
    
    //valid 'cause unbounded
    //wildcard is reifiable type
    if(lt2 instanceof List<?>)
      System.out.println("valid!");
      
    //In some cases, the compiler can cast parameterized
    //type 'cause the compiler knows that an argument type
    //is valid to be casted to that argument type
    List<String> lt3 = new LinkedList<>();
    LinkedList<String> linkedList = 
    (LinkedList<String>) lt3;
  }
}
6.)We can't create arrays of parameterized types.
import java.util.List;
import java.util.ArrayList;
public class SampleClass{

  public static void main(String[]args){
    //invalid: can't create arrays of 
    //parameterized types
    //List<String>[] lt1 = new ArrayList<>[2];
	
    //valid but with raw type warning
    List<String>[] lt2 = new ArrayList[2];
	
    //valid since unbounded wildcard is reifiable
    List<?>[] lt3 = new ArrayList<?>[2];
	
    //Let's pretend that array of parameterized
    //type is valid. If that's the case, then
    //our generic algorithm will be more prone
    //to ArrayStoreException
    /*
    //pretend this is a valid syntax
    Object[] lt = new List<String>[2]; 
    //ok
    lt[0] = new ArrayList<String>(); 
    //ArrayStoreException should be thrown
    //but it can't be detected 'cause
    //the compiler thinks this is valid
    //'cause Object is a superclass of
    //ArrayList
    lt[1] = new ArrayList<Integer>(); 
    */
  }
}
7.) Generic class can't extend Throwable and any subclasses of Throwable. Also, we can't use type parameter as type in catch block and throw keyword. However, we can use type parameter next to throws keyword.
public class SampleClass{
  
  /*
  static <T,U extends Exception>void castType(T type){
    try{
      String s = (String)type;
    }
    //error
    catch(U e){
    }
  }
  */
  
  //valid: type variable can be used next to throws keyword
  <T,U extends RuntimeException> void meth() throws U{
    throw new RuntimeException("Test!");
    //invalid
    //throw new U("Error");
  }
  
  public static void main(String[]args){
  }
}


//error
/*
class ClassA<T> extends Throwable{
}
*/

//error
/*
class ClassB<T> extends RuntimeException{
}
*/
8.)We can't overload generic methods with equal method signature after erasure.
import java.util.List;
public class SampleClass{
  
  //Invalid generic method overloading
  //Before erasure
  void meth(List<String> lt){
  }
  
  void meth(List<Integer> lt){
  }
  
  After Erasure
  /*
  void meth(List lt){
  }
  
  void meth(List lt){
  }
  */
  
  public static void main(String[]args){
  }
}

Wednesday, June 23, 2021

Java Tutorial: Exception Handling

Chapters

Java Exception Handling

Exception handling is the mechanism in java that handles run-time errors in a program. Exception is an abnormal condition that disrupts the normal flow of a program. Java has classes that are thrown everytime our program encounters these runtime errors. These classes are the subclasses of Throwable class which is the superclass of all exceptions and errors in the java language.

Throwable class has direct subclasses which are Error and Exception. These subclasses are conventionally used to indicate that exceptional situations have occurred.
Errors are serious and unrecoverable; regular applications shouldn't try to catch errors.
Exceptions are abnormal conditions that regular applications should try to catch.

In java, exceptions/errors can be categorized into two sections: Checked Exceptions and Unchecked Exceptions.

Checked Exceptions: These exceptions are checked at compile-time. We are required to handle these exceptions and java won't compile our source code if we don't handle these exceptions. For example, we use FileReader class in our source code. This class may throw IOException and FileNotFoundException; IOException and FileNotFoundException are checked exceptions.

That's why java requires us to handle IOException or FileNotFoundException everytime we use FileReader class and other stream classes that may throw these exceptions.
Example

import java.io.*;

public class SampleClass{

  public static void main(String[]args){
    String path = "C:"+File.separator+"tmp"+
                  File.separator+"test.txt";
    //This code below may throw FileNotFoundException
    //'cause file in the path might not be existed
    //this source code won't compile cause 
    //FileNotFoundException is not handled
    FileReader reader = new FileReader(path);
  }
}
Unchecked Exceptions: These exceptions are checked at run-time. Unlike checked exceptions, we are not required to handle unchecked exceptions. For example, if we try to access an array index that is less than 0 or greater than the length of the array then, java will throw an ArrayIndexOutOfBoundsException which is an unchecked exception.
Example

//This source code will be compiled just fine.
//However, once we run the compiled version
//of this source code then java will implicitly
//throw an ArrayIndexOutOfBoundsException and terminate
//the program

public class SampleClass{

  public static void main(String[]args){
  
    String[] str = {"a","b"};
    
    System.out.println("First Element: " +str[0]);
    
    //This println() below makes java
    //throw an ArrayIndexOutOfBoundsException
    //Since we didn't handle the exception
    //our program will terminate right
    //here and won't execute any codes
    //next to this println()
    System.out.println(str[2]);
    
    //this won't be executed
    System.out.println("Second Element: " + str[1]);
  }
}
You might ask: "How can we determine if an exception is checked or unchecked?". According to Throwable class documentation: " For the purposes of compile-time checking of exceptions, Throwable and any subclass of Throwable that is not also a subclass of either RuntimeException or Error are regarded as checked exceptions."

In other words, Error,RuntimeException and their subclasses are unchecked exceptions, Other than that, all exceptions are checked exceptions. Don't be confused between RuntimeExceptions and exceptions that are thrown at run-time.

Generally, all exceptions are thrown at run-time. One of the purposes of RuntimeException class is to separate some exceptions that are checked at run-time from exceptions that are checked at compile-time.

Don't be confused between the programming term "exception" and the Exception class in java. Exception(or run-time error), in programming term, is an abnormal condition that disrupts the normal flow of a program whereas Exception class separates "exceptions" that should be caught from "exceptions" that shouldn't be caught.

Don't be confused between compile-time errors and exceptions. compile-time errors are errors thrown during compile-time whereas exceptions are thrown during run-time. Generally, compile-time errors and run-time errors are called errors.

Compile-time warning is like compile-time error. However, warnings don't stop the compiler from compiling our source code. Nevertheless, we should resolve warnings to have clean code.

try-catch clause

try-catch clause is used to enclose a statement or group of statements that may throw an exception. we use try-catch clause to catch an exception and to avoid abrupt termination of our program due to uncaught exceptions.
public class SampleClass{
  
  public static void main(String[]args){
    String str = null;
    
    try{
      
      //Switch block will throw 
      //NullPointerException 'cause
      //switch doesn't accept null
      //values
      switch(str){
      
        case "A":
        break;
        
        case "B":
        break;
      }
      
      System.out.println("This code won't be executed");
    }
    catch(NullPointerException e){
      
      if(str == null)
         System.out.println("NullPointerException has been"
                        +" thrown in the try block because"
                        +" str is null");
      else
        System.out.println("NullPointerException has been"
                        +" thrown in the try block");
    }
    
    //This code will be executed
    System.out.println("Exiting program...");
    
  }
}
As you can see in the example above, statements next to switch block are not executed because once the execution encounters an exception in the try block, it will leave the try block and it will check if the exception in the catch parentheses matches the exception thrown in the try block.

If match then, the execution will execute the catch block and execute the rest. Otherwise, It means that we failed to catch the exception thrown in the try block so, catch block is not executed and the program is terminated.

We can't put a checked exception in catch block if there's no code in the try block that will throw that checked exception.
import java.io.*;
public class SampleClass{

  public static void main(String[]args){
  
    try{
      String str = "A";
      str = str.concat(str);
    }
    catch(IOException e){
      System.out.println("IOException!");
    }
  }
}
The example above will throw a compile-time error: exception IOException is never thrown in the body of corresponding try statement.

Multiple Catch Blocks

There are times that we need to catch multiple exceptions. In order to do that, we can write multiple catch blocks in one try block. We need to follow an order when writing multiple catch blocks, the most specific exception comes first and the most general exception comes last. We can compare this order to the subclass-to-superclass order,if you will, where subclass is the most specific and superclass is the most general.
import java.io.*;
public class SampleClass{

  public static void main(String[]args){
  
    String path = "C:"+File.separator+"test"
                 +File.separator+"read.txt";
    System.out.println("Path: " + path);
    try{
      //if the path is invalid then, this code
      //below will throw a FileNotFoundException
      FileReader reader = new FileReader(path);
      
      
      String content = "";
      //This will make the concat() to throw a
      //NullPointerException
      //String content = null;
      
      int c = 0;
      while((c = reader.read()) != -1)
         content = content.concat(String.valueOf((char)c));
         
      //Always close streams after you're done using it
      reader.close();
      
      System.out.println(content);
      
    }
    //This exception is a subclass of
    //IOException
    catch(FileNotFoundException e){
      System.out.println("File Not Found!");
    }
    //This exception is a subclass of
    //RuntimeException
    catch(NullPointerException e){
      System.out.println("null value is improperly"
                        +" used!");
    }
    //Exception is the parent class of
    //all throwable exceptions
    //(except for exceptions in Error class)
    catch(Exception e){
      System.out.println("An exception has been"+
                         " caught!");
    }
  }
}
We will encounter a compile-time error if we try to put a superclass exception first before its subclasses.
import java.io.*;
public class SampleClass{

  public static void main(String[]args){
  
    String path = "C:"+File.separator+"test"
                 +File.separator+"read.txt";
    System.out.println("Path: " + path);
    try{
      FileReader reader = new FileReader(path);
      
      String content = "";
      //This will make the concat() to throw a
      //NullPointerException
      //String content = null;
      
      int c = 0;
      while((c = reader.read()) != -1)
         content = content.concat(String.valueOf((char)c));
         
      //Always close streams after you're done using it
      reader.close();
      
      System.out.println(content);
      
    }
    //error parent class exception must come 
    //after its subclasses
    catch(Exception e){
      System.out.println("An exception has been"+
                         " caught!");
    }
    catch(FileNotFoundException e){
      System.out.println("File Not Found!");
    }
    catch(NullPointerException e){
      System.out.println("null value is improperly"
                        +" used!");
    }
    
  }
}
Multiple Exceptions in Catch Block

We can put multiple exceptions in one catch block. We're going to use the "|" symbol which is also a Bitwise Inclusive OR(|) Operator to separate multiple exceptions.
import java.io.*;
public class SampleClass{

  public static void main(String[]args){
  
    String path = "C:"+File.separator+"test"
                 +File.separator+"read.txt";
    System.out.println("Path: " + path);
    try{
      FileReader reader = new FileReader(path);
      
      String content = "";
      //This will make the concat() to throw a
      //NullPointerException
      //String content = null;
      
      int c = 0;
      while((c = reader.read()) != -1)
         content = content.concat(String.valueOf((char)c));
         
      //Always close streams after you're done using it
      reader.close();
      
      System.out.println(content);
      
    }
    //multiple exceptions in a catch block
    catch(NullPointerException | IOException e){
          //we use printStackTrace()
          //to get more detailed message
          //about the thrown exception
          e.printStackTrace();
    }
    
  }
}
In the example above, we use the printStackTrace() method. This method returns a stack trace about the exception that calls printStrackTrace(). This method can give us detailed information about the exception, especially the line number where the exception occurs. printStackTrace() doesn't terminate a program after it's invoked.

We can't put a superclass and its subclass at the same catch parentheses.
    catch(FileNotFoundException | IOException e){
          //we use printStackTrace()
          //to get more detailed message
          //about the thrown exception
          e.printStackTrace();
    }
This code above will throw a compile-time error because FileNotFoundException is a subclass of IOException.

The Finally Block

We can add another block next to the try-catch clause and that block is the finally block. Finally block will be executed whether there's an exception caught or not or there's an uncaught exception in the try clause. This block is preferrably used to close streams or connections.
import java.io.*;
public class SampleClass{

  public static void main(String[]args){
  
    String path = "C:"+File.separator+"test"
                 +File.separator+"read.txt";
    System.out.println("Path: " + path);
    FileReader reader = null;
    try{
      reader = new FileReader(path);
      
      String content = "";
      
      int c = 0;
      while((c = reader.read()) != -1)
         content = content.concat(String.valueOf((char)c));
      
      System.out.println(content);
      
    }
    catch(NullPointerException | IOException e){
          e.printStackTrace();
    }
    //finally block. This block will be executed
    //whether there's an exception caught or not
    //in the try-catch clause
    finally{
      //nested try-catch
      //Always close streams after you're done using it
      try{ 
        if(reader != null)
          reader.close();
      }
      catch(IOException e){ e.printStackTrace(); }
      System.out.println("Stream is closed.");
    }
    
  }
}
We put the close() method in the finaly block. finally block will ensure that the close() method will be executed whether there's an exception, uncaught exception or no exception at all, once the execution exits the try block.

We can use the finally block without the catch block.
public class SampleClass{

  public static void main(String[]args){

    try{
      
      String s = "A";
      s = s.concat(s);
      System.out.println("Try block...");
      System.out.println(s);
    }
    finally{
      String t = "B";
      t = t.concat(t);
      System.out.println("finally block...");
      System.out.println(t);
    }
  }
}
If a method exits because of return statement, a finally block will still be executed.
public class SampleClass{

  static int divide(int num1, int num2){
  
    try{
    
      return num1/num2;
    }
    finally{
      System.out.println("Finally block!!");
    }
    
  }

  public static void main(String[] args){
  
    int num = SampleClass.divide(4,2);
    System.out.println(num);
  }
}

Result
Finally block!!
2
Nested try-catch

try-catch(-finally) clause can be nested in another try-catch(-finally) clause.
import java.io.*;
public class SampleClass{

  public static void main(String[]args){
    
    String path = "C:"+File.separator+
                  "test"+File.separator+
                  "test.txt";
    FileWriter writer = null;
    try{
    
      String text1 = "text1";
      
      //nested try-catch
      try{
        String text2 = null;
        text1 = text1.concat(text2);
      }
      catch(NullPointerException e){
        System.out.println("text2 is null "
                          +"can't concatenate with"
                          +" text1");
      }
      System.out.println("Writing text to text file...");
      
      //note: instantiating FileWriter automatically
      //overwrites the specified file in the path
      //assigned to FileWriter. Overwritten file is
      //unrecoverable
      writer = new FileWriter(path);
      writer.write(text1);
      
      System.out.println("Operation Successful!");
    }
    catch(IOException e){
      System.out.println("An IOException"
                        +" has been caught!");
      e.printStackTrace();
    }
    finally{
     System.out.println("Closing Stream...");
     //nested try-catch
     try{ writer.close(); }
     catch(IOException e){
       e.printStackTrace();
     }
    }
    System.out.println("Stream is closed!");
  }
}
try-with-resources

We can use try-with-resources if we're handling streams or connections and we want those to be closed automatically. The streams declared in the try-with-resources parentheses will be closed automatically whether java is done executing the try-with-resources or an exception(caught/uncaught) occurs. Note: streams and connections must have an implementation of AutoCloseable or Closeable interface.
import java.io.*;
public class SampleClass{

  public static void main(String[]args){
    
    String readPath = "C:"+File.separator+
                      "test"+File.separator+
                      "read.txt";
    String writePath = "C:"+File.separator+
                       "test"+File.separator+
                       "write.txt";
                       
    //Use semicolon to separate two or more
    //resources. Otherwise, omit the semicolon
    //Single resource
    //try(FileReader reader = new FileReader()){}
    //multiple resources
    try(FileReader reader = new FileReader(readPath);
        FileWriter writer = new FileWriter(writePath)){
        
        String content = "";
        int c = 0;
        while((c = reader.read()) != -1)
         content = content.concat(String.valueOf((char)c));
      
        System.out.println(content);
        
        writer.write(content);
        System.out.println("Operation Completed!");
    }
    catch(IOException e){
      e.printStackTrace();
    }
    //We can still add the finally block as part of 
    //try-with-resources
    finally{
      System.out.println("finally block is executed!");
    }
    
  }
}
The throw Keyword

We can use the throw keyword to create a throw statement and to explicitly throw an exception.
General Form: throw new exception-name();

public class SampleClass{

  public static void main(String[]args){
  
    String str = null;
    
    if(str == null)
      throw new NullPointerException("Null"
                +" values are not allowed here!");
    else
      str = str.concat(str);
  }
}
We can also use the throw keyword to rethrow exceptions. Remember, Throwing exception using throw keyword is still a thrown exception and needs to be caught if we don't want our program to terminate.

The throws Keyword

We can use the throws keyword to declare exceptions in a methods/constructors. We can declare multiple exceptions by separating them using comma(,).
import java.io.*;
import java.text.ParseException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
public class SampleClass{
  
  //We can declare an exception in a constructor
  //SampleClass() throws IOException{ //codes }
  
  //use throws keyword to declare IOException
  //in this method
  String readFile() throws IOException{
    String readPath = "C:"+File.separator+
                      "test"+File.separator+
                      "read.txt";
                      
    FileReader reader = new FileReader(readPath);
    String content = "";
    int c = 0;
    while((c = reader.read()) != -1)
         content = content.concat(String.valueOf((char)c));
      
    System.out.println(content);
    reader.close();
    
    return content;
  }
  
  //separate multiple exceptions by comma(,)
  //when declaring them in a method
  void writeData(String data) throws IOException,ParseException{
    String writePath = "C:"+File.separator+
                       "test"+File.separator+
                       "write.txt";
    
    String strDate = "2000-10-10";
    DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
    Date date = dateFormat.parse(strDate);
    
    FileWriter writer = new FileWriter(writePath);
    
    writer.write(data);
    writer.close();
    System.out.println("Data Is Written In " + date);
  }
  
  public static void main(String[]args){
    SampleClass sc = new SampleClass();
    
    try{sc.writeData(sc.readFile());}
    catch(IOException | ParseException e){
      e.printStackTrace();
    }
    
  }
}
If a declared exception is thrown in method block and it's not caught then, that method will drop the thrown exception down the call stack. A call stack is a list of chained methods/constructors that are called. The methods/constructors at the top of the stack is the last method that is called in the chain.

One of the uses of throws keyword is to propagate checked exceptions, which we will discuss later. That's why you will see that throws keyword is mostly used in conjunction with checked exceptions.

Exception Propagation

Exception Propagation is a process of throwing exceptions down the call stack. A call stack is a list of chained methods that are called. Unchecked exceptions are automatically propagated whereas checked exceptions need to be propagated by declaring exceptions in method/constructor blocks by using the throws keyword.

First off, Let's create an example to demonstrate unchecked exception propagation.
public class SampleClass{
  
  String addLetter(String message){
    String letterToAdd = null;
    
    return message.concat(letterToAdd);
  }
  
  String addNumber(String message){
    String numToAdd = "1";
    
    return message.concat(numToAdd);
  }
  
  void displayMessage(String message){
    System.out.println("Message: " + message);
  }
  
  public static void main(String[]args){
    SampleClass sc = new SampleClass();
    
    String message = "My Message";
    try{
      sc.displayMessage(sc.addNumber(sc.addLetter(message)));
    }
    catch(NullPointerException e){
      e.printStackTrace();
    }
    
  }
}

Result:
java.lang.NullPointerException
    at java base/java.lang.String.concat(String.java:1465)
    at SampleClass.addLetter(SampleClass.java:6)
    at SampleClass.main(SampleClass.java:24)
By using printStackTrace() we can see where the exception happens. In the example above, NullPointerException is thrown from java.lang.String.concat() method then, concat() with exception is called in addLetter() then, addLetter() throws the exception in addNumber() then, in displayMessage() and then, the exception is caught by try-catch in main().

We also see that we don't need to declare NullPointerException in method block in order to be propagated in the main().

Now, let's try propagating checked exception.
import java.io.*;
public class SampleClass{
  
  //Declare IOException here because we
  //don't want to catch IOException in this method
  //we want to propagate IOException in
  //the main() method and catch the exception
  //there
  String readFile() throws IOException{
    String readPath = "C:"+File.separator+
                      "test"+File.separator+
                      "read.txt";
                      
    FileReader reader = new FileReader(readPath);
    String content = "";
    int c = 0;
    while((c = reader.read()) != -1)
         content = content.concat(String.valueOf((char)c));
      
    System.out.println(content);
    reader.close();
    
    return content;
  }
  
  //We are required to declare IOException here because
  //we are using readFile() in this method block and
  //we need to redeclare IOException in order to
  //move down the thrown IOException in readFile()
  //into displayMessage() then in the main() to be
  //caught.
  void displayMessage() throws IOException{
    System.out.println("Message: " + readFile());
  }
  
  public static void main(String[]args){
    SampleClass sc = new SampleClass();
    
    try{ sc.displayMessage(); }
    catch(IOException e){
      e.printStackTrace();
    }
    
  }
}
readFile() will throw an IOException, FileNotFoundException to be exact, if the specified file in the path of readPath doesn't exist. Exception propagation is useful if you want multiple exceptions from different sources to be caught in a single try-catch(-finally) clause.

Wrapping and Rethrowing Exception

We can wrap an exception into another exception to show a more specific exception.
import java.io.*;
public class SampleClass{

  public static void main(String[]args){
  
    String writePath = "C:"+File.separator+
                       "test"+File.separator+
                       "write.txt";
    
    String content = null;
    File file = new File(writePath);
    try{
      content = content.concat(content);
      FileWriter writer = new FileWriter(file);
      writer.write(content);
      System.out.println("Content has been written!");
    }
    catch(IOException | NullPointerException e){
      
      if(content == null)
        throw new RuntimeException(e);
      else
        e.printStackTrace();
    }
    
  }
}
In the example above, we wrap "e" into RuntimeException. Now, java will throw a RuntimeException and the cause of the RuntimeException which is the NullPointerException.

We can rethrow an exception if we want to do some kind of task, for example logging, before throwing the exception.
public class SampleClass{

  public static void main(String[]args){
 
    try{
      String str = null;
      str = str.concat(str);
    }
    catch(NullPointerException e){
      System.out.println("Add to log file: str is null");
      throw e;
    }
    
  }
}
This way, we preserve the stack trace of the exception that will be displayed on the console while adding the exception to a log.

Note: Unchecked exceptions can be easily rethrown. However, checked exceptions are not preferrable to be rethrown.
import java.io.*;
public class SampleClass{

  public static void main(String[]args){
 
    try{
      FileReader reader = new FileReader("C:\\Temp\\test.txt");
    }
    catch(IOException e){
      System.out.println("Add to log file: an IOException");
      //error
      throw e;
      
      //fix but a bad code design
      //unnecessary use of try-catch
      //clause
      /*
      try{
          throw e;
      }catch(IOException s){s.printStackTrace()}
      */
      
    }
    
  }
}

Custom Exception

We can create our own exception by overriding Exception or RuntimeException class. If we want to create a checked exception then, we override the Exception class. If we want to create an unchecked exception then, we override the RuntimeException class.
public class SampleClass{
  
  //LongPathException is checked exception. Therefore, we
  //need to declare it in this method block since we want to
  //catch it in the main()
  void checkPathLength(String path) throws LongPathException{
    if(path.length() > 255)
      throw new LongPathException("Path name is very long!");
  }
  
  //PanelNotCompatibleException is an unchecked exception.
  //Therefore, we don't need to declare it in this method
  //block because unchecked exception is automatically 
  //propagated
  void initMainPanel(SystemPanel sp){
    
    if(sp instanceof MainPanel)
       System.out.println("MainPanel" + sp.getName() +
                          "has been initialized!");
    else
     throw new PanelNotCompatibleException(sp.getName()+" is not a main"+
                                           " panel!");
    
  }
  
  public static void main(String[]args){
    SampleClass sc = new SampleClass();
    
    String path = "C:\\Users\\User1\\My Documents\\Some"+
                  " Files that needed to be opened\\"+
                  " Bunch of files that can be opened"+
                  " if we want to\\files with very long"+
                  " pathnames\\This file has a very long"+
                  " long long long long long long long"+
                  " long long long long long long long"+
                  " long long long long long long long"+
                  " long long long long long long long"+
                  " path name.txt";
    try{
      //This will throw the PanelNotCompatibleException
      sc.initMainPanel(new SubPanel("Sub Panel"));
      
      //This will throw the LongPathException
      sc.checkPathLength(path);
    }
    catch(LongPathException | PanelNotCompatibleException e){
      e.printStackTrace();
    }
    
  }
}

class SystemPanel{
  private String name;
  
  SystemPanel(String name){
    this.name = name;
  }
  
  String getName(){
    return name;
  }
  
}

class MainPanel extends SystemPanel{
  private int panelID = 1;
  
  MainPanel(String name){
    super(name);
  }
  
  int getID(){
    return panelID;
  }
}

 class SubPanel extends SystemPanel{
  private int panelID = 2;
  
  SubPanel(String name){
    super(name);
  }
  
  int getID(){
    return panelID;
  }
}

//checked exception
class LongPathException extends Exception{
  
  LongPathException(String message){
    super(message);
  }
}

//unchecked exception
class PanelNotCompatibleException extends RuntimeException{

  PanelNotCompatibleException(String message){
    super(message);
  }
}
Suppressed Exceptions

Normally, a method/constructor with declared exception or try block can only throw one exception at a time. Though, Sometimes, two or more exceptions can be thrown in try-catch(-finally) or try-with-resources, if that happens then some exceptions might be suppressed.
public class SampleClass{

  public static void main(String[]args){
  
    try{
      //NullPointerException
      String s = null;
      s = s.concat(s);
    }
    finally{
      //ArithmeticException: can't be
      //divided by 0
      int num = 10/0;
    }
    
  }
}
In the example above, two exceptions are thrown: NullPointerException and ArithmeticException. However, only the AritmeticException will be shown on the console because NullPointerException has been suppressed. Let's try try-with-resources.
import java.io.*;
public class SampleClass{

  public static void main(String[]args){
                       
    try(DummyResource dr = new DummyResource()){
      dr.divideByZero();

    }
    finally{
     throw new IllegalArgumentException("Bad Argument!");
    }
    
  }
}

class DummyResource implements AutoCloseable{
  
  void divideByZero(){
    int num = 10/0;
  }
  
  @Override
  public void close(){
    throw new NullPointerException("Thrown by implicit close()");
  }
}
In the example above, IllegalArgumentException suppressed ArithmeticException thrown from divideByZero() which suppressed NullPointerException thrown from implicit close().

To get suppressed exceptions, we will use the getSuppressed() method from Throwable class.
import java.io.*;
public class SampleClass{

  public static void main(String[]args){
    
    try(DummyResource dr1 = new DummyResource("resource1");
        DummyResource dr2 = new DummyResource("resource2")){
       dr1.divideByZero();

    }
    catch(ArithmeticException e){
      System.out.println("Suppressed Exceptions");
      for(Throwable t : e.getSuppressed())
        System.out.println(t.toString());
        
      System.out.println("rethrow ArithmeticException");
      throw e;
      
    }
    
  }
}

class DummyResource implements AutoCloseable{
  private String name;
  
  DummyResource(String name){
    this.name = name;
  }
  
  void divideByZero(){
    int num = 10/0;
  }
  
  @Override
  public void close(){
    throw new NullPointerException("Thrown from " + name +
                                   " resource");
  }
}
The suppressed exceptions are specified in the exception message of the thrown exception. getSuppressed() is optional in this example. By the way, I'm using java 11. I don't know if this feature is working with other versions.
Exception Snapshot


If we want to suppress an exception, we can use the addSuppressed() method in Throwable class.
public class SampleClass{

  public static void main(String[]args){
    
    NullPointerException npe = new 
                               NullPointerException(
                               "str is null!");
    String str = null;
    try{
      
      str = str.concat(str);
    }
    catch(Exception e){
      
      if(str == null){
        npe.addSuppressed(e);
        throw npe;
      }
      else e.printStackTrace();
    }
  }
}

Sunday, June 20, 2021

Java Tutorial: enum Class

Chapters

Java enum Class

Reference: The Java™ Tutorials
Java enum is a special type of class that helps us create fixed set of constants which are called enum constants. Enum constants are variables that have values that can't be changed, just like final variables. Multiple enum constants must be separated by (,)comma.
public class SampleClass{

  public static void main(String[]args){
    //declaring enum references
    Jobs jobs = Jobs.SORT_DOCUMENTS;
    FileType type = FileType.TXT;
    
    //enum constants are instances of
    //their class.
    if(jobs instanceof Jobs)
      System.out.println("jobs is an instance "+
                         "of Jobs");
    if(type instanceof FileType)
      System.out.println("type is an instance "+
                         "of FileType");
  }
  
  //enum can be declared as inner class
  enum Jobs{
    //comma-separated constants
    SORT_DOCUMENTS,SORT_ZIP
  }
}

//enum can be declared as top-level class
enum FileType{
  //comma-separated constants
  TXT,PDF,ZIP
}
Note: enum constants are implicitly public, static and final by default.

enum Class Constructor

Like standard classes, enum class can have constructor. Though, we can't instantiate enum class. Enum constants are implicitly initialized with their class reference.
Example 

//This enum declaration
enum FileType{
  TXT,PDF,ZIP
}

//can be depicted like this
enum FileType{

  public static final FileType TXT = new FileType();
  public static final FileType PDF = new FileType();
  public static final FileType ZIP = new FileType();
}
Now, let's create an example to demonstrate enum constructor.
Note: Access modifier for enum constructor must be package-private(default) or private only.
public class SampleClass{

  public static void main(String[]args){
    int heroHP = BaseStats.HP.VALUE;
    int heroMP = BaseStats.MP.VALUE;
    int heroLvl = 1;
    
    if(heroLvl > 1){
      heroHP += 5 * heroLvl;
      heroMP += 10 * heroLvl;
    }
    
    System.out.println("Hero Level: " + heroLvl);
    System.out.println("Hero HP: " + heroHP);
    System.out.println("Hero MP: " + heroMP);
    System.out.println("Hero has taken percentage damage"
                      +" based on base stats!");
    System.out.println("HP and MP have been reduced!");
                      
    heroHP -= BaseStats.HP.percentDmgBase(.10f);
    heroMP -= BaseStats.MP.percentDmgBase(.25f);
    System.out.println("Hero HP: " + heroHP);
    System.out.println("Hero MP: " + heroMP);
    
  }
}

enum BaseStats{
  //java requires enum constants
  //to be defined first prior to any fields
  //or methods.
  //
  //When declaring enum constants with
  //constructor, don't forget to add 
  //parentheses() and also the semicolon(;)
  //at the end of the last enum constant.
  //
  //adding semicolon enables enum class for 
  //additional class members like variables
  //and methods. Try omitting the semicolon
  //here and you will encounter a convoluted error
  //
  //If the enum constructor has parameter-list
  //then the parentheses of enum constants must
  //have argument-lists that match the
  //parameter-list
  HP(1000),MP(500);
  
  final int VALUE;
  BaseStats(int value){
    VALUE = value;
  }
  
  int percentDmgBase(float percent){
    return (int)(VALUE * percent);
  }

}
Enum constructor won't instantiate its enum class constants until one of them is used.
public class SampleClass{

  public static void main(String[]args){
    System.out.println("SampleClass");
    //uncomment the codes below and the enum
    //constructor will instantiate its constants
    //FileType type1 = FileType.PDF;
    //System.out.println("type1: "+type1);
    
    //We can also get an enum constant
    //in another enum constant
    //FileType type2 = FileType.PDF.ZIP;
    //System.out.println("type2: "+type2);
  }
}

enum FileType{
  TXT(),PDF(),ZIP();
  
  FileType(){
    System.out.println(ordinal());
  }
}

java.lang.Enum in enum Class

java.lang.Enum class is implicitly inherited by enum class. That's why we can use some methods in java.lang.Enum. Let's create an example.
public class SampleClass{

  public static void main(String[]args){
  
    Task task = Task.MOVE;
    
    //ordinal() returns the constant position
    //in its enum declaration starting from 0
    System.out.println(task.ordinal());
    
    //name() returns the name of the constant
    String name = task.name();
    
    //or use toString()
    //String name = task.toString();
    
    System.out.println(name);
    
    //or directly use the reference in println()
    //'cause toString() has already been overriden
    //in enum class
    System.out.println("Object: " + task);
    //The result of this println() below is strange 
    //'cause toString() in object returns a hashcode-like
    //address of object
    System.out.println("Object: " + new SampleClass());
  }
}

enum Task{
  MOVE,COPY
}
Due to the fact that java.lang.Enum is implicitly inherited by enum class, enum class can't explicitly extend any class. Also, enum class can't be extended.

You might ask: "Object class is implicitly inherited by every class but classes can still explicitly extend a class then, Why enum class can't explicitly extend a class because java.alang.Enum is implicitly inherited?"

In my opinion, the reason is because implicit inheritance of java.lang.Enum in enum class has explicit effect whereas implicit inheritance of Object class in every class doesn't have explicit effect. This is just my opinion. Take it with a grain of salt.

values() Method in enum Class

In enum class, there's a static method that is implicitly defined and that method is the values() method. values() method returns an array of enum constants that are declared in an enum class.
public class SampleClass{

  public static void main(String[]args){
  
    for(Task task : Task.values())
      System.out.println(task);
  }
}

enum Task{
  MOVE,COPY,DELETE,MOVE_TO_TRASH
}

enum Constants in switch Block

We can use the enum constants as criteria in switch block.
public class SampleClass{

  public static void main(String[]args){
    DocumentType docType = DocumentType.PDF;
    
    //We don't need to include the enum class name
    //when using enum constants as criteria in switch
    //block
    switch(docType){
      
      case EPUB:
      System.out.println("Document Type is EPUB");
      break;
      
      case MOBI:
      System.out.println("Document Type is MOBI");
      break;
      
      case PDF:
      System.out.println("Document Type is PDF");
      break;
      
      case AZW3:
      System.out.println("Document Type is AZW3");
      break;
    }
    
  }
}

enum DocumentType{
  EPUB,MOBI,PDF,AZW3
}

Implementing Interface in enum Class

we know that enum classes can't extend classes. Though, enum classes can implement interfaces.
public class SampleClass{

  public static void main(String[] args){
    int heroHP = BaseStats.HP.VALUE;
    int heroMP = BaseStats.MP.VALUE;
    int heroLvl = 1;
    
    if(heroLvl > 1){
      heroHP += 5 * heroLvl;
      heroMP += 10 * heroLvl;
    }
    
    System.out.println("Hero Level: " + heroLvl);
    System.out.println("Hero HP: " + heroHP);
    System.out.println("Hero MP: " + heroMP);
    System.out.println("Hero has taken percentage damage"
                      +" based on base stats!");
    System.out.println("HP and MP have been reduced!");
                      
    heroHP -= BaseStats.HP.percentDmgBase(.10f);
    heroMP -= BaseStats.MP.percentDmgBase(.25f);
    System.out.println("Hero HP: " + heroHP);
    System.out.println("Hero MP: " + heroMP);
  }
}

interface ComputeDamage{

  int percentDmgBase(float percent);
}

//implementing interface in enum class
enum BaseStats implements ComputeDamage{
  HP(1000),MP(500);
  
  final int VALUE;
  BaseStats(int value){
    VALUE = value;
  }
  
  @Override
  public int percentDmgBase(float percent){
    return (int)(VALUE * percent);
  }

}