Practice 07
The material for the practical course¶
In this practice session we will learn about new object-oriented constructs. Our example will be an application dealing with animals, including terrestrial, aquatic, predatory and herbivorous animals.
To do this, we will need some classes that we have already talked about writing and instantiating before, so we will take this knowledge as familiar. Anyone who might still have problems with creating simple classes, please refer to the previous practical material (classes and heredity). And for lecture material, you should definitely check out the lecture on interfaces and abstract classes.
Since we will be dealing with animals, we will need an Animal
class. An animal should have a name, satiety (how hungry it is), and strength.
public class Animal {
private String name;
private int satiety;
private int strength;
public Animal(String name) {
this.name = name;
this.satiety = 100;
this.strength = 0;
}
//.. getters and setters
public String makesNoise(){
return "";
}
}
Abstract¶
Above you can see that the makesNoise()
method has no practical use, it is only created to be able to be overridden in child classes, so that for example in an array Animal
you can call the makesNoise()
method on elements.
Abstract methods¶
We've encountered this before, and we can actually avoid situations like this, since the only point of the makesNoise()
method is to be able to override it in child classes. However, there are two problems with this at the moment: it works in the class Animal
, even though we don't know what sound an animal in general makes; and it is not currently required to override it in the child class, since the methods we are using are either overridden or not, there is no obligation to override them, and we can even forget about them if we are not careful.
The keyword abstract
is used to solve this problem. You can prepend it to your methods, and in return you don't have to implement them in the class. With the abstract
keyword, we say that we don't want to implement one (or more) specific method(s) in the class, we just want to say that we will have to override this/these specific method(s) in the child class.
Abstract classes¶
However, if we put the abstract
keyword in front of a method, then the class must also be abstract. The keyword for this is also abstract
, which must be included in the class declaration.
This is necessary because if you have a class that is abstract, then it is not instantiable, and if you have a method that has no body, then it should not really be instantiated. Therefore, if the class has at least one abstract method, then the whole class must be abstract. Of course, this only means that it cannot be instantiated in its current form, and then the child class should implement the abstract methods (otherwise the child class should be abstract too).
Of course, in addition, abstract classes may have data members, and even methods that are implemented. Let's rewrite the Animal
class!
public abstract class Animal {
private String name;
private int satiety;
private int strength;
public Animal(String name) {
this.name = name;
this.satiety = 100;
this.strength = 0;
}
// getters, setters
public abstract String makesNoise();
}
This way, we've achieved that our class doesn't have a method that we can't provide normal functionality for, instead we just say that the makesNoise()
method in child classes must be overridden if we want a class that we can instantiate. If we try to instantiate an abstract class, we get a compilation error.
You can also make a class inherit from an abstract class, while it does not necessarily implement all the inherited abstract methods, in which case that class will also be abstract. Let's make two child classes, TerrestrialAnimal
and AquaticAnimal
, which will be the ancestor classes of terrestrial and aquatic animals, respectively:
public abstract class TerrestrialAnimal extends Animal{
public TerrestrialAnimal(String name) {
super(name);
}
private int numberOfLegs;
public int getnumberOfLegs() {
return numberOfLegs;
}
public void setnumberOfLegs(int numberOfLegs) {
this.numberOfLegs = numberOfLegs;
}
}
public abstract class AquaticAnimal extends Animal {
public AquaticAnimal(String name) {
super(name);
}
@Override
public String makesNoise() {
return "cannot be heard underwater";
}
}
Now we can create specific animals, depending on whether they live in water or on land. Let's make two more classes of ancestors: Herbivore
and Predator
, which will be the ancestors of herbivores and predators.
public class Predator {
public void eats(Animal what){}
public void rests(int howMuch){}
}
public class Herbivore {
public void eats(){}
}
Again, this does not look very good. Instead, we can use the trick we just learned, i.e. make the methods of the class, and the class itself abstract. This would be fine, but now we do run into one of Java's limitations, the lack of multiple inheritance, since a Dog
cannot be both Animal
and Predator
. Abstracting is a workable construction, but it may not be the best one in this case.
Interface¶
Because if a class has neither data members nor implemented methods, we are effectively just talking about an interface that tells the class that extends it what methods to implement if we want to be able to instantiate it, but we abstracted it. In Java, there is also a keyword for the interface, which will be interface
. This is to indicate to the world that this class
contains only method declarations, so there is certainly no method implemented, nor any data members. This is the purpose of the keyword interface
.
public interface Herbivore {
public abstract void eats();
}
public interface Predator {
public abstract void eats(Animal what);
public abstract void rests(int howMuch);
}
Earlier, it was mentioned that there is no multiple inheritance in Java, but any number of interfaces can be implemented by a class, so a bear can be both Animal
and Predator
. In fact, animals that are omnivores can implement both a Predator
and a Herbivore
interface.
In an interface, all methods are implicitly abstract, so you do not need to write it out.
public interface Herbivore {
public void eats();
}
public interface Predator {
public void eats(Animal what);
public void rests(int howMuch);
}
Interfaces are not inherited (unless an interface is inherited from another interface), but implemented, that is, methods declared in an interface are implemented in the class that implements the interface.
public class Chicken implements Herbivore {
@Override
public void eats() {
System.out.println(this.getName() + " is satiated with seeds.");
this.setSatiety(100);
}
}
However, if you don't want to fully implement an interface, you can do that too, but you will have methods that you don't implement. Thus, such classes will also need to be abstract
keyworded, since they will be abstract classes (i.e., classes with methods that are abstract).
The Chicken class is both TerrestrialAnimal
and Herbivore
, so it can be derived from the TerrestrialAnimal
class, but it can also implement the Herbivore
interface.
public class Chicken extends TerrestrialAnimal implements Herbivore {
public Chicken(String name) {
super(name);
setnumberOfLegs(2);
}
@Override
public void eats() {
System.out.println(this.getName() + " is satiated with seeds.");
this.setSatiety(100);
}
@Override
public String makesNoise() {
return "cluck cluck";
}
}
instanceof, getClass()¶
instanceof¶
Create a Herd
class with animals in it. Each animal object has a specific type, since the Animal
class is not instantiable, but the actual created animals are in memory.
Also, create a acceptIntoHerd()
method that takes an animal as a parameter, and if there is room in the herd, that animal is accepted into it, otherwise not.
public class Herd {
private Animal[] animals;
private int herdPopulation;
private int currentPopulation;
public Herd(int herdPopulation) {
this.herdPopulation = herdPopulation;
this.currentPopulation = 0;
animals = new Animal[herdPopulation];
}
public boolean acceptIntoHerd(Animal what) {
if (currentPopulation < herdPopulation) {
animals[currentPopulation] = what;
currentPopulation++;
return true;
}
return false;
}
@Override
public String toString() {
String str = "They are in the herd:";
for (int i = 0; i < currentPopulation; i++) {
str += animals[i].toString();
}
return str;
}
You may need the specific type of the elements of an array of ancestor type (or whatever else, we will see later). This is not a problem, since at runtime we will know the specific type of each array element, since the actual object is in memory.
To retrieve this, the instanceof
keyword can be used, whose syntax may seem strange at first: object instanceof Class
.
This is a logical expression, so it can be used in if
, for example, and returns whether a given object is of a particular class or one of its ancestors, so if there is an array of type Animal
called animals
, then any element of that array would return true for the expression animals[i] instanceof Animal
.
getClass()¶
The getClass()
is one of the methods inherited from the ancestor class Object
(here you can find the javadoc of the Object class), which returns a built-in type, Class
, and this method cannot be overridden in children, since it is a final
method. If we have a class instead of an object, we can also retrieve its type using the Class.class
statement. Thus, we can retrieve the actual class of an object by using the object.getClass()
method. This method only allows comparison with the actual class, so if there is an array of type Animal
named animals
, then any element of that array would return false for the expression animals[i].getClass() == Animal.class
, since Animal
in our case is an abstract class, which, since it cannot be instantiated, cannot be the actual type of any object.
Tasks¶
Completing Animals¶
- Create three animals of your choice in the
animals
package, including terrestrial, aquatic, herbivore, predator. Implement all required methods! - As it stands, the class
Herd
can contain any animal (for example, a shark, a lion and a chicken can be in the same herd). This may not be correct in real life, so fix it:- Make sure that a herd can only contain terrestrial animals or only aquatic animals. The first animal to be tested is the first animal that wants to be in the herd, and if it is a terrestrial animal, then only terrestrial animals can be in the herd, if it is aquatic, then only aquatic animals.
- Make sure that a herd can only contain herbivores or predators. This is done by testing the first animal that wants to be in the herd, and if it is a herbivore, then only herbivores can be in the herd, if it was a predator, only predators.
- Perform the previous two tasks in bulk, i.e. only animals living in the same place (terrestrial, aquatic) and having the same lifestyle (herbivore, carnivore) should be included in a herd.
- Solve the above problems by heredity, which should be special
Herd
classes.
Shapes¶
Write a Shape
interface containing the methods for area and circumference calculation, and the PI constants needed for the calculation. Create a Circle
, a Rectangle
, and a Square
class, each implementing the Shape
interface.
1. Put several of these shapes into an array, then
1. print out the characteristics of the shapes in turn,
2. determine the average area of the shapes,
3. count how many circles there are among shapes.
Trees¶
-
create the class
Tree
!- Store its height, trunk diameter, whether it is dead, and its leaf size in an array.
- Have a parametric constructor that expects height, trunk diameter and maximum number of leaves. By default, the tree is not dead and has no leaves.
- Create a grows() method that increases the height of the tree by 1.
- Make a timePassing() method that expects an integer and returns nothing. The method should not be implemented in the class.
- Override toString() method in the class.
-
Create the class
Pinetree
, which is a special tree!- Store what type of needle-leaves the pine tree has.
- Make a suitable parametric constructor that also sets the type of the pine needles.
- Override the grows() method, which also increases the diameter of the pine tree trunk by 1%.
- Define timePassing(), which calls grows() as many times as the value of the parameter. Also, add the same number of leaves to the tree.
- Override the toString() method in the class.
-
Create the interface
Deciduous
with a single method:- leafShedding(), which takes no parameters and returns an integer.
-
Create the class
Oaktree
, which is a tree! Oak trees are known to be deciduous.- For an oak tree, we store how big it can grow.
- Create a suitable parametric constructor that also sets the maximum size.
- Override the grows() method, which works as follows: if the height of the oak tree has not yet reached its maximum size, increase its height by 5%. Be sure to increase the diameter of its trunk by 3%. If the tree has reached its maximum size, it should have a 20% chance of dying.
- Implement the leafShedding() method, which sets the size of all leaves to 0.
- Implement the timePassing() method, which should work as follows:
- If the tree is dead, then nothing happens
- If the tree has no leaves, then create 50 new leaves (or if the tree can have less than that, then that many), and call the grows() method as many times as the value of the parameter
- If the tree has leaves, then drop them
- Override the toString() method in the class.
-
Create a main function in which to create an array with size 5, randomly populated with pine or oak trees, and simulate the passage of time.