Welcome to Lesson 15!
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.
VisibilityTo
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
|
- Video (Ignore the advertisement in the middle - use powerpoint on similar instead)
2. 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:
A Generic Version of the Twin Class
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"); } }
- 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:
- 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") - 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?
}
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.
- 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
|