Welcome to Lesson 15!


Learning Objectives

By the end of today's class, you should know...


  • How to construct UML class diagrams?
  • What is a generic type and what benefit does it offer?
  • Why were generics added to the language in JDK 1.5?
  • How do you define a generic class, interface and method?
  • What is a raw type?
  • What is type erasure?
  • What are some restrictions when using generics?


1. UML Class Diagrams

  • For your Lab 15, and your project presentation, you will need to create UML class diagrams to represent your ideas for the project
  • Below is an example of a class diagram with all parts labeled.
  • Class diagrams are made up of boxes with connectors.
  • The boxes represent the classes (or interfaces) and the connectors the relationships between the classes (or interfaces)
  • You can use the below diagram as a model for your project.
    • Note that it is a simplified example (i.e. some methods and variables have been redacted) from Lab 13, the Twice Sold Tales bookstore assignment.
UML Class Diagram for Twice Sold Tales Lab

  • In the diagram, classes are represented with boxes that contain three compartments:

    • The top compartment contains the name of the class. The name is displayed in bold and centered, and the first letter is capitalized (just like the name of a class should be in your project).
    • The middle compartment contains the attributes (member variables) of the class. They are left-aligned and the first letter is lowercase.
    • The bottom compartment contains the operations the class can execute (the member methods). They are also left-aligned and the first letter is lowercase.
  • There are different types of connectors between the boxes:

UML Connectors

Visibility

To specify the visibility of a class member (i.e. any variable or method), these notations must be placed before the member's name:

+ Public
- Private
# Protected
/ Derived (can be combined with one of the others)
~ Package


2. Generics

Introduction to Generics

  • Generics are a way to make code more reusable
  • Generics enable types (classes and interfaces) to be parameters when defining classes, interfaces and methods.
  • Much like the more familiar formal parameters used in method declarations, type parameters provide a way for you to re-use the same code with different inputs.
    • The difference is that the inputs to method parameters are values, while the inputs to type parameters are types.
  • Code that uses generics has many benefits over non-generic code:
    • Stronger type checks at compile time.
    • Elimination of certain types of casts.
  • Generics were introduced into Java with JDK 5.
  • For example, prior to JDK 5, the Comparable Interface was defined like so:
package java.lang;

public interface Comparable {
    int compareTo(Object o);
}

  • As we saw in the previous section, Comparable has since been redefined to make use of generics:
package java.lang;

public interface Comparable<T> {
    int compareTo(T t);
}
  • In the above definition, the <T> represents a generic type.
  • Later this type will be replaced with a concrete data type.
class Dog implements Comaparable<Dog> { //T replaced with type Dog
  • By convention, a single capital letter such as E or T is used to denote a generic type, making it easy to recognize a generic type as opposed to a variable name.
  • With generics, we can make our code more flexible.
  • For example, in early lessons, you learned about the ArrayList, which is a generic class:
public class ArrayList<E>
  • Recall that when we instantiate new ArrayList, we need to indicate in <> what kind of data is being stored.
ArrayList<String> list1 = new ArrayList<String>();
ArrayList<Integer> list2 = new ArrayList<Integer>();
ArrayList<Double> list3 = new ArrayList<Double>();
ArrayList<Student> list4 = new ArrayList<Student>();
  • Note that generic types must be reference types. Therefore, you cannot replace a generic type with a primitive type such as int, double or char.
  • Instead, you must use their reference type counterparts: Integer, Double, Character, and Boolean.


Why Were Generics Introduced to the Language?

  • Prior to JDK 1.5, certain runtime errors were common - leading to classCastExceptions - because programmers had to use type Object to give their classes and interfaces the flexibility to work with data of different types.
  • For example, before the introduction of generics, the ArrayList class stored data of type Object.
  • Using Object for this purpose can lead to mistakes like the one shown below:
ArrayList list = new ArrayList();
list.add("abc");
list.add(new Integer(5)); //Will compile

for(int i = 0; i < list.size(); i++){
    //type casting leading to ClassCastException at runtime
    String str = (String) list.get(i);
}
  • With the introduction of generics, these types of mistakes could be caught at compile-time rather than run-time:
ArrayList<String> list = new ArrayList<String>();
list.add("abc");
list.add(new Integer(5)); //Will NOT compile

for(int i = 0; i < list.size(); i++) {
    String str = list.get(i); //no casting needed now
}
  • Compiler errors are easier to track down (error messages reported by compiler with line numbers) than errors at run-time.
  • Also, casting is not needed, as the compiler keeps track of the type of data stored in the generic class.

Defining Generic Classes and Interfaces

  • Consider another example of a class called Twin, that stores some data, and has some accessor and mutator methods.
  • Prior to the introduction of generics, we might have defined this class to store data of type Object.
public class Twin {
    private Object twin1;
    private Object twin2

  public void setTwin1(Object object) {
        twin1 = object;
    }

    public void setTwin2(Object object) {
        twin2 = object;
    }

    public Object getTwin1() {
        return twin1;
    }
    

    public Object getTwin12() {
        return twin2;
    }

}
  • Since its methods accept or return an Object, you are free to pass in whatever you want, provided that it is not one of the primitive types.
public class TwinTest {
    public static void main(String args[]) {
        Twin twins = new Twin();
        twins.setTwin1("Mo");
        twins.setTwin2(555);
       
        String s1 = (String) twins.getTwin1(); //method returns type Object, so must cast
        String s2 = (String) twins.getTwin2(); //Mistake!
        System.out.println("Twin 1: " + s1 + "\nTwin2: " + s2);
    }
}

  • There is no way to verify, at compile time, how the class is used.
  • One part of the code may place an Integer in the Twin and expect to get String out of it, while another part of the code may mistakenly pass in a String, and expect an Integer to be returned.
  • The above code produces a run-time error:
classCastException thrown at runtime


A Generic Version of the Twin Class

  • A generic class is defined with the following format:
    public class name<T1, T2, ..., Tn> {
    
  • The type parameter section, delimited by angle brackets (<>), follows the class name.
  • It specifies the type parameters (also called type variables) T1, T2, ..., and Tn.
  • To update the Twin class to use generics, you create a generic type declaration by changing the code public class Twin to public class Twin<T>.
  • The above statement declares the type variable, T, that can be used anywhere inside the class.
  • With this change, the Twin class becomes:
public class Twin<T> {
    private T twin1;
    private T twin2;

    public void setTwin1(T t) {
        twin1 = t;
    }

    public void setTwin2(T t) {
twin2 = t;
    }

    public T getTwin1() {
return twin1;
    }

    public T getTwin2() {
return twin2;
    }

}

  • The T stands in place of the type that will later be declared when an instance of the class is created.
  • Then, our test class would be updated as follows 
public class TwinTest {
    public static void main(String args[]) {
        Twin<String> twins = new Twin<String>();
        twins.setTwin1("Mo");
        //twins.setTwin2(555); //will not compile
        twins.setTwin2("Bo");
       
        String s = twins.getTwin2(); //no cast needed
        System.out.println(s);
    }
}

The Diamond

  • Since JDK 1.7, it is also possible to declare variables of generic classes using the diamond:
public class TwinTest {
    public static void main(String args[]) {
        Twin<String> twins = new Twin<>();
        twins.setTwin1("Mo");
        //twins.setTwin2(555); //will not compile
        twins.setTwin2("Bo");
       
        String s = twins.getTwin2(); //no cast needed
        System.out.println(s);
    }
  }

  • Declaring the type to be String once is enough for the compiler to know the type being stored

Multiple Type Parameters - Updated Twin.java Class

  • It is also possible to store data of more than one generic type inside of the same class.
  • For example, below is a revision of the Twin class, where each twin stores a different type of data

public class Twin<T, E> {
    private T twin1;
    private E twin2;
   
    public void setTwin1(T t) {
        twin1 = t;
    }
   
    public void setTwin2(E e) {
        twin2 = e;
    }
   
    public T getTwin1() {
        return twin1;
    }
   
    public E getTwin2() {
        return twin2;
    }

}
  • Notice that we provide the two generic types as a comma-separated List
public class TwinTest {
    public static void main(String args[]) {
        Twin<String, Integer> twins = new Twin<>();
        twins.setTwin1("Mo");
        twins.setTwin2(555);
       
    }
}
  • Interfaces can also be generic, like the Comparable interface, as well as the interface defined below:
public interface Twin<T, E> {
   
    void printTwin1();
    void printTwin2();
   
    T getTwin1();
    E getTwin2();

}

Generic Methods

  • In some cases, there is no purpose in making an entire class generic, if only a generic method is required.
  • To create a generic method, you place the <T> (or whatever letter has been selected) before the return type of the method.
  • Then, you are free to use this type throughout the method to stand in for any data type:
public class ArrayWrapper {

//rest of class

public static <T> void printArray(T[] array) {
    for (int i = 0; i < array.length - 1; i++) {
        System.out.println(array[i]);
    }
}
}

Raw Types

  • A generic class or interface that is used without specifying a concrete type is called a raw type.
  • The raw type of Twin<T> is just Twin.
  • Raw types allow backwards compatibility with earlier version of Java (before the existence of generics)
  • You can use a generic class without specifying a concrete type:
public class TwinTest {
    public static void main(String args[]) {
        Twin twins = new Twin();
        twins.setTwin1("Mo");
        twins.setTwin2("Bo");
       
    }
}
  • The result is roughly the equivalent of the following:
public class TwinTest {
    public static void main(String args[]) {
        Twin<Object> twins = new Twin<Object>();
        twins.setTwin1("Mo");
        twins.setTwin2("Bo");
       
    }
}

Type Erasure

  • Generics are implemented using an approach called type erasure
  • The compiler uses the generic information to compile the code, but erases it afterwards.
  • Thus the generic information is not available at runtime.
  • Once the compiler has verified that the generic type is used safely, it converts the generic type to an Object type.
  • For example, given the below generic class Box:

public class Box<E> {
    private E contents;

    public Box() {
        contents = null;
    }
   
    public E getContents() {
        return contents;
    }

    public void getContents(E contents) {
        this.contents = contents;
    }
}
  • Upon compilation, the type E is replaced with the type Object
 
public class Box {
    private Object contents;

    public Box() {
        contents = null;
    }
   
    public Object getContents() {
        return contents;
    }

    public void getContents(Object contents) {
        this.contents = contents;
    }
    }
  • Because of type erasure, generic classes remain compatible with legacy Java code written before generics became part of the language.
  • However, due to type erasure, there are restrictions about how generic types can be used:

restriction 1:

  • You cannot call new to construct an instance of a generic type. In other words, you cannot make statements like the following:
E e = new E();

  • The reason for this restriction is that the new is executed at runtime, but the generic type E is not available at runtime.

restriction 2:

  • For the same reason as restriction 1 above, you cannot instantiate an array of generic types. The below will not compile:

E[] array = new E[10];
  • You can sidestep this limitation by instantiating an array of Object types and then cast it to type E[] as shown below:
E[] array = (E[]) new Object[10];
  • However, casting to type E[] causes an unchecked compiler warning. The warning indicates that the compiler is not certain a ClassCastException will not be thrown at runtime.
  • It is acceptable to suppress this warning using the following tag:
@SuppressWarnings("unchecked")

restriction 3:

  • A generic type parameter is not allowed in a static context.
  • Since static variables and methods are shared among all objects of a class -- and these classes can now store different types of data -- it will not be clear what datatype should be used to replace the generic type:
  • The below statements will not compile:
public static E twin;
public static E updateTwin(E twin) {...}

Consider why:
public static void main(String[] args) {
    Twins<String> twins1 = new Twins<>();
    Twins<Integer> twins2 = new Twins<>();
    Twins<Double> twins3 = new Twins<>();

    System.out.println(Twin.twin); //What type of data is twin?

}


More Information:


Activity 15.1: Happy Birthday! (10 pts)

  • Create a new generic class, as follows:
    • The name of the class is GiftBag
    • It stores two private member variables:
      • One is of generic type and is named gift
      • The other is of type boolean and is named open
    • It has two constructors:
      • A default constructor, which sets gift to be null, and open to be false
      • A one argument constructor, which takes in a value for gift (and assigns this value to gift) and assigns open to be false
    • An accessor method called getGift, which returns the current value of gift
    • An accessor method called isOpen, which returns the current value of open
    • A mutator method called openGift that sets the value of open to be true
    • A toString() method which returns the value returned by calling the toString() of gift
  • Additionally, in the same project folder as GiftBag.java, open up a new class called Phone.java (from one of our class examples).
  • Copy and paste the below code into that class file:
/**
 * Phone.java
 * @author
 * CIS 36B
 */

public class Phone {
    private String brand;
    private String model;
    private double price;
   
    /**Constructor(s)*/
   
    public Phone() {
        this("Unknown brand", "Unknown model", 0.0);
    }
   
    public Phone(String theBrand, String theModel, double thePrice) {
        brand = theBrand;
        model = theModel;
        price = thePrice;
    }
   
    public void makeCall() {
        System.out.println("Making call from Phone.");
    }
   
    @Override public String toString() {
        return "Brand: " + brand 
            + "\nModel: " + model
            + "\nPrice: " + price;
    }
}
  • Next, locate Candy.java from a prior activity and copy and paste the contents into a new file named Candy.java in your new project folder, or use the one provided below

/**
 * Candy.java
 * @author
 * CIS 36B
 */
import java.util.ArrayList;

public abstract class Candy {
    private int numCalories;
    private ArrayList<String> ingredients;
    private static int numPieces = 0;
   
    public Candy() {
        this(0, new ArrayList<String>());
    }
   
    public Candy(int numCalories, ArrayList<String> ingredients) {
        this.numCalories = numCalories;
        this.ingredients = ingredients;
    }
   
    public Candy(Candy c) {
        numCalories = c.numCalories;
        ingredients = new ArrayList<String>(c.ingredients);
    }
   
    public static int getNumPieces() {
        return numPieces;
    }
   
    public int getNumCalories() {
        return numCalories;
    }
   
    public static void updateNumPieces() {
        numPieces++;
    }
   
    public void setNumCalories(int numCals) {
        numCalories = numCals;
    }
   
    public void addIngredient(String ingredient) {
        ingredients.add(ingredient);
    }
   
    public abstract void printCandyGreeting();
   
    @Override public String toString() {
        String result = "Total Calories " + numCalories;
        result += "\nIngredients:\n";
        for (int i = 0; i < ingredients.size(); i++) {
            result += ingredients.get(i) + "\n";
        }
        return result;
               
    }
   
    @Override public boolean equals(Object o) {
        if (o == this) {
            return true;
        } else if (! (o instanceof Candy)) {
            return false;
        } else {
            Candy c = (Candy) o;
            return this.numCalories == c.numCalories
                    && this.ingredients.equals(c.ingredients);
        }
   
    }
   
}

  • Next, copy and paste the child class MilkyWay.java into a new file in your project folder. Locate the one from your prior activity or use the code provided below:
/**
 * MilkyWay.java
 * @author
 * CIS 36B
 */
public class MilkyWay extends Candy{
   
    private String size;
    private String flavor;
   
    public MilkyWay() {
        super();
        size = "unknown size";
        flavor = "unknown flavor";
    }
   
    public String getSize() {
        return size;
    }
   
    public String getFlavor() {
        return flavor;
    }
   
    public void setSize(String size) {
        this.size = size;
    }
   
    public void setFlavor(String flavor) {
        this.flavor = flavor;
    }
   
    public void printCandyGreeting() {
        System.out.println("Welcome to the Milky Way " + flavor + "!");
    }
   
    @Override public String toString() {
        return "Flavor: " + flavor
                + "\nSize: " + size
                + "\n" + super.toString();
    }
   
}

  • Lastly, below is a simple test class for the generic GiftBag.java class.
  • Copy and paste the contents into a new file called Birthday.java:

/**
 * Birthday.java
 * @author
 * CIS 36B, Activity 15.1
 */

import java.util.ArrayList;
public class Birthday {
    public static void main(String[] args) {
        ArrayList<GiftBag> gifts = new ArrayList<GiftBag>(); //using the raw type
        
        Phone iphone = new Phone("iPhone", "7", 150.99);
        GiftBag<Phone> gift1 = new GiftBag<>(iphone);
        gifts.add(gift1);
        
        MilkyWay midnight = new MilkyWay();
        midnight.setSize("MINI");
        midnight.setFlavor("MIDNIGHT");
        midnight.addIngredient("SEMISWEET CHOCOLATE");
        midnight.addIngredient("CORN SYRUP");
        midnight.addIngredient("SUGAR");
        midnight.addIngredient("HYDROGENATED PALM KERNEL OIL");
        midnight.addIngredient("SKIM MILK");
        midnight.setNumCalories(38);
        GiftBag<MilkyWay> gift2 = new GiftBag<>(midnight);
        gifts.add(gift2);
        
        GiftBag<String> gift3 = new GiftBag<>("Confetti!");
        gifts.add(gift3);
        
        System.out.println("Happy birthday!\n\nYour gifts:");
        for (GiftBag g: gifts) {
            System.out.println(g.getGift().toString() + "\n");
        }
        
    }
    
}

  • Run the code to make sure your GiftBag.java class is working properly.
  • You should get the below output:
Sample output:

Happy birthday!

Your gifts:
Brand: iPhone
Model: 7
Price: 150.99

Flavor: MIDNIGHT
Size: MINI
Total Calories 38
Ingredients:
SEMISWEET CHOCOLATE
CORN SYRUP
SUGAR
HYDROGENATED PALM KERNEL OIL
SKIM MILK


Confetti!

  • Next, it is your job to add one more GiftBag into the gifts ArrayList.
  • Note that the type of gift in the GiftBag must be of a different data type than the other 3 gifts (no Phones, MilkyWays or Strings)
  • When you have successfully added your gift, and gotten the correct output, upload GiftBag.java and Birthday.java, as well as any additional new class used in Birthday.java, to Canvas.


Wrap Up: 

  • Answer the Practice Exam Questions for this lesson on Canvas.


Upcoming Assignments:

  • Activity 15.1 due Tuesday at 11:59pm
  • Lesson 15 Practice Exam Questions due Tuesday at 11:59pm
  • Quiz 7 due Friday at 11:59pm
  • Lab 8 due next Monday at 11:59pm