Friday, June 11, 2021

Java Tutorial: Typecasting(Objects)

Chapters

Typecasting(Objects)

You need to have an understanding of inheritance to understand this topic. In this topic, we're going to discuss casting an object type to another type which is called typecasting.

I already covered typecasting of primitive types in my previous tutorials. When we instantiate a class, an object is gonna be created. Objects have types and their types are based on the types of classes in which they're created.

Typecasting can be done if two classes have parent-to-child relationship. A sub class is a sub type and a super class is a super type. Typecasting has two types: upcasting and downcasting. Variables(fields) types of instance being converted remain the same during typecasting.

Upcasting

Upcasting is the process of casting a sub type to a super type. Target type is implicitly done by the compiler.
public class SampleClass{

  public static void main(String[]args){
    BallPoint bp = new BallPoint(1, 100, "Pointy");
    System.out.println("bp content");
    System.out.println("Name: " + bp.name);
    System.out.println("Ink Level: " + bp.inkLvl);
    System.out.println("Pen No: " + bp.getPenNo());
    System.out.println();
  
    //This statement is valid but redundant.
    //Pen pen = (Pen)bp;
  
    //No need to explicitly write the target type
    //the compiler will do it for us
    Pen pen = bp;
  
    System.out.println("pen content");
    System.out.println("Pen No: " + pen.getPenNo());
    System.out.println("Ink Level: " + pen.inkLvl);
    //error sub class members are not visible to
    //super class
    //System.out.println("Name: " + pen.name);
  }
  
}

abstract class Pen{
  private int penNo;
  int inkLvl;
  
  Pen(int penNo,int inkLvl){
    this.penNo = penNo;
    this.inkLvl = inkLvl;
  }
  
  public int getPenNo(){ return penNo; }
}

class BallPoint extends Pen{
  String name;
  
  BallPoint(int penNo,int inkLvl,String name){
    super(penNo,inkLvl);
    this.name = name;
  }
}
In the example above, we can see that we can assign a BallPoint object type to a Pen type variable. BallPoint is sub class of Pen and Pen is super class of BallPoint. Thus, upcasting is successful.

We can also see in the example above that pen variable can only access Pen class members. That is expected because when we upcast a sub type to a super type, a sub type will become a super type, thus, the sub type will have the same behavior as super type.

When a sub type is casted to a super type, sub class members still exist, they're just not accessible. They can be accessible again once the sub type that becomes super type is casted back to sub type.

When we cast a sub class that overrode a method to a parent class then, overriding method in sub class will override the the overriden method in parent class once the casting is successful.
public class SampleClass{

 public static void main(String[]args){

    ClassA a = new ClassB();
    a.greetings();
 }
}

class ClassA{

  void greetings(){
    System.out.println("Hello! This is ClassA!");
  }
}

class ClassB extends ClassA{

  void greetings(){
    System.out.println("Hello! This is ClassB!");
  }
}

Result
Hello! This is ClassB!

Downcasting

Downcasting is the process of casting a super type to a sub type. Target type must be explicitly defined.
public class SampleClass{

  public static void main(String[]args){
    BallPoint bp = new BallPoint(1, 100, "Pointy");
    System.out.println("bp content");
    System.out.println("Name: " + bp.name);
    System.out.println("Ink Level: " + bp.inkLvl);
    System.out.println("Pen No: " + bp.getPenNo());
    System.out.println();

    Pen pen = bp;
    System.out.println("pen content");
    System.out.println("Pen No: " + pen.getPenNo());
    System.out.println("Ink Level: " + pen.inkLvl);
    System.out.println();
    
    //Downcasting
    //It's a good practice to use instanceof
    //everytime we downcast a type
    if(pen instanceof BallPoint){
      BallPoint bp2 = (BallPoint)pen;
      System.out.println("bp2 content");
      System.out.println("Name: " + bp2.name);
      System.out.println("Ink Level: " + bp2.inkLvl);
      System.out.println("Pen No: " + bp2.getPenNo());
      System.out.println();
    }
    
  }
  
}

abstract class Pen{
  private int penNo;
  int inkLvl;
  
  Pen(int penNo,int inkLvl){
    this.penNo = penNo;
    this.inkLvl = inkLvl;
  }
  
  public int getPenNo(){ return penNo; }
}

class BallPoint extends Pen{
  String name;
  
  BallPoint(int penNo,int inkLvl,String name){
    super(penNo,inkLvl);
    this.name = name;
  }
}
In the example above, we can see that the object that is referenced to pen variable which is a former sub type retains its accessibility to all of its members once it returns back to its original type. Also, use instanceof keyword to avoid ClassCastException.

ClassCastException is thrown at runtime if the type that we're casting is not compatible with the target type. instanceof keyword checks if the reference type on its left is compatible with the type on its right.

You might wonder: "Why downcasting needs the target type to be explicitly defined whereas upcasting doesn't?" The reason, in my opinion, is that downcasting is more problematic than upcasting.

In primitive types, downcasting may cause data loss. For example, we wanna downcast long to int, long is 64 bits and int is 32 bits, in order to cast long to int, long needs to lose half of its bits.

In object typecasting, downcasting is more prone to ClassCastException than upcasting. That's why it's preferrable to check if one type is compatible with another type by using instanceof before downcasting a type. Take a look at this example.
public class SampleClass{

  public static void main(String[]args){
    
    BallPoint bp = new BallPoint(1, 100, "Pointy");
    System.out.println("bp content");
    System.out.println("Name: " + bp.name);
    System.out.println("Ink Level: " + bp.inkLvl);
    System.out.println("Pen No: " + bp.getPenNo());
    System.out.println("Ball Point Type: " + bp.ballPointType);
    System.out.println();
    
    Pen pen = bp;
    System.out.println("pen content");
    System.out.println("Pen No: " + pen.getPenNo());
    System.out.println("Ink Level: " + pen.inkLvl);
    System.out.println();
    
    //ClassCastException
    RollerBall bp2 = (RollerBall)pen;
    System.out.println("bp2 content");
    System.out.println("Name: " + bp2.name);
    System.out.println("Ink Level: " + bp2.inkLvl);
    System.out.println("Pen No: " + bp2.getPenNo());
    System.out.println("Roller Ball Type: " + bp2.rollerBallType);
    System.out.println();
  }
  
}

abstract class Pen{
  private int penNo;
  int inkLvl;
  
  Pen(int penNo,int inkLvl){
    this.penNo = penNo;
    this.inkLvl = inkLvl;
  }
  
  public int getPenNo(){ return penNo; }
}

class BallPoint extends Pen{
  String name;
  String ballPointType = "default";
  
  BallPoint(int penNo,int inkLvl,String name){
    super(penNo,inkLvl);
    this.name = name;
  }
}

class RollerBall extends Pen{

  String name;
  String rollerBallType = "default";
  
  RollerBall(int penNo,int inkLvl,String name){
    super(penNo,inkLvl);
    this.name = name;
  }
}
In the example above, A ClassCastException happened when the program attempted to cast Pen object type to RollerBall object type. When you think about it, it is legal because Pen is the super class of RollerBall. Thus, Pen can be downcasted to RollerBall.

However, the object type that is referenced to "bp" variable is only compatible with BallPoint and Pen classes because when we instantiate the BallPoint class, it inherits its parent class which is Pen class in this case, thus inheriting its parent type.

Since java 14, we can use this type of syntax for downcasting.
public class SampleClass{

  public static void main(String[]args){
    Pen pen = new BallPoint(1, 100, "Pointy");
    
    //Shorthand syntax for downcasting
    //bp2 is defined in the if parentheses
    //no need to use the traditional
    //downcasting syntax
    if(pen instanceof BallPoint bp2){
      System.out.println("bp2 content");
      System.out.println("Name: " + bp2.name);
      System.out.println("Ink Level: " + bp2.inkLvl);
      System.out.println("Pen No: " + bp2.getPenNo());
      System.out.println();
    }
    
  }
  
}

abstract class Pen{
  private int penNo;
  int inkLvl;
  
  Pen(int penNo,int inkLvl){
    this.penNo = penNo;
    this.inkLvl = inkLvl;
  }
  
  public int getPenNo(){ return penNo; }
}

class BallPoint extends Pen{
  String name;
  
  BallPoint(int penNo,int inkLvl,String name){
    super(penNo,inkLvl);
    this.name = name;
  }
}
Typecasting Using Class<T> Class

Note: You need to understand generics to understand the syntax of Class<T> class.
Another way to cast objects is using the Class<T> Class. Don't be confused with the java class and "Class<T>" class. Class<T> class is a class in java that provides tools to gather information about classes.

This type of typecasting is pretty advance and it's preferrably used in java reflection. This <T> that we're seeing is part of the syntax of generic types which is also a pretty advance topic.

We're not gonna fully discuss Class<T> class here. We're just gonna focus on its two methods: cast() and isInstance().
public class SampleClass{

  public static void main(String[]args){
    BallPoint bp = new BallPoint(1, 100, "Pointy");
    System.out.println("bp content");
    System.out.println("Name: " + bp.name);
    System.out.println("Ink Level: " + bp.inkLvl);
    System.out.println("Pen No: " + bp.getPenNo());
    System.out.println();

    Pen pen = bp;
    System.out.println("pen content");
    System.out.println("Pen No: " + pen.getPenNo());
    System.out.println("Ink Level: " + pen.inkLvl);
    System.out.println();
    
    //Downcasting using Class<T>
    if(Pen.class.isInstance(pen)){
      BallPoint bp2 = BallPoint.class.cast(pen);
      System.out.println("bp2 content");
      System.out.println("Name: " + bp2.name);
      System.out.println("Ink Level: " + bp2.inkLvl);
      System.out.println("Pen No: " + bp2.getPenNo());
      System.out.println();
    }
  }
}

abstract class Pen{
  private int penNo;
  int inkLvl;
  
  Pen(int penNo,int inkLvl){
    this.penNo = penNo;
    this.inkLvl = inkLvl;
  }
  
  public int getPenNo(){ return penNo; }
}

class BallPoint extends Pen{
  String name;
  
  BallPoint(int penNo,int inkLvl,String name){
    super(penNo,inkLvl);
    this.name = name;
  }
}
In this example, we use the class keyword to refer to a specific class. For example, Pen.class refers to the Pen class. Once we refer to a class, we can use Class<T> methods like the cast() and isInstance(). isInstance() is equivalent to instanceof keyword and cast() is equivalent to traditional typecasting.

No comments:

Post a Comment