小練習:
Modify the previous HeavenlyBody example so that the HeavenlyBody
class also has a “bodyType” field. This field will store the
type of HeavenlyBody (such as STAR, PLANET, MOON, etc).
You can include as many types as you want, but must support at
least PLANET and MOON.
For each of the types that you support, subclass the HeavenlyBody class
so that your program can create objects of the appropriate type.
Although astronomers may shudder at this, our solar systems will
allow two bodies to have the same name as long as they are not the
same type of body: so you could have a star called “BetaMinor” and
an asteroid also called “BetaMinor”, for example.
Hint: This is much easier to implement for the Set than it is for the Map,
because the Map will need a key that uses both fields.
There is a restriction that the only satellites that planets can have must
be moons. Even if you don’t implement a STAR type, though, your program
should not prevent one being added in the future (and a STAR’s satellites
can be almost every kind of HeavenlyBody).
Test cases:
1. The planets and moons that we added in the previous video should appear in
the solarSystem collection and in the sets of moons for the appropriate planets.
2. a.equals(b) must return the same result as b.equals(a) — equals is symmetric.
3. Attempting to add a duplicate to a Set must result in no change to the set (so
the original value is not replaced by the new one).
4. Attempting to add a duplicate to a Map results in the original being replaced
by the new object.
5. Two bodies with the same name but different designations can be added to the same set.
6. Two bodies with the same name but different designations can be added to the same map,
and can be retrieved from the map.
參考答案:
HeavenlyBody.java
package SetChallenge;import java.util.HashSet;
import java.util.Set;public abstract class HeavenlyBody {
private final Key key;
private final double orbitalPeriod;
private final Set<HeavenlyBody> satellites; public static Key makeKey(String name, BodyTypes bodyType) {
return new Key(name, bodyType);
} public HeavenlyBody(String name, double orbitalPeriod, BodyTypes bodyType) {
this.orbitalPeriod = orbitalPeriod;
this.satellites = new HashSet<>();
this.key = new Key(name, bodyType); } public double getOrbitalPeriod() {
return orbitalPeriod;
} public Key getKey() {
return key;
} public boolean addSatellite(HeavenlyBody satellite) {
return this.satellites.add(satellite);
} public Set<HeavenlyBody> getSatellites() {
return new HashSet<>(this.satellites);
} @Override
public final boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj instanceof HeavenlyBody) {
Key objKey = ((HeavenlyBody) obj).getKey();
return this.key.equals(objKey);
}
return false;
} @Override
public final int hashCode() {
return this.key.hashCode();
} @Override
public String toString() {
return getKey().getName() + ": " + getKey().getBodyType() + ", " + getOrbitalPeriod();
}
enum BodyTypes {
PLANET, DWARF_PLANET, MOON
} public static final class Key {
private String name;
private BodyTypes bodyType; private Key(String name, BodyTypes bodyType) {
this.name = name;
this.bodyType = bodyType;
} public String getName() {
return name;
} public BodyTypes getBodyType() {
return bodyType;
} @Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if ((obj == null) || (obj.getClass() != this.getClass())) {
System.out.println("obj is null or not equal!");
return false;
} String objName = ((HeavenlyBody.Key) obj).getName();
BodyTypes objBodyType = ((HeavenlyBody.Key) obj).getBodyType();
return this.name.equals(objName) && this.bodyType.equals(objBodyType);
} @Override
public int hashCode() {
return this.name.hashCode() + this.bodyType.hashCode() + 37;
} @Override
public String toString() {
return name + ": " + bodyType;
}
}
}
class Planet extends HeavenlyBody {
public Planet(String name, double orbitalPeriod) {
super(name, orbitalPeriod, BodyTypes.PLANET);
} @Override
public boolean addSatellite(HeavenlyBody moon) {
if (moon.getKey().getBodyType() == BodyTypes.MOON) {
return super.addSatellite(moon);
}
return false; }
}
class DwarfPlanet extends HeavenlyBody {
public DwarfPlanet(String name, double orbitalPeriod) {
super(name, orbitalPeriod, BodyTypes.DWARF_PLANET);
}
}
class Moon extends HeavenlyBody {
public Moon(String name, double orbitalPeriod) {
super(name, orbitalPeriod, BodyTypes.MOON);
}
}
The reason is that a map can’t have duplicate keys. The 6th requirement of this challenge is to allow bodies with the same name but different types to be added (types are Planet, Moon, DwarfPlanet, and such).
As you may have noticed, our Map only contains String keys, so an attempt to store bodies with the same name and different types would fail since you can’t have duplicate keys. What I’m trying to say is, let’s say you have 2 keys: Pluto (type Planet) and Pluto (type DwarfPlanet). For example, if you wrote [map.put(“Pluto”, new Planet(“Pluto”, 90.8);] then you won’t be able to do [map.put(“Pluto”, new DwarfPlanet(“Pluto”, 64.6));] because they both have the same String key “Pluto”.
So you need to find a way to overcome this and it is storing objects that contain both name and type as keys. A Key object contains name and bodyType so we will be able to add 2 keys that contain the same name (Pluto) but of different type (one is Planet and the other is DwarfPlanet). The reason for this is the equals() method inside Key class, the method compares the 2 object’s name then it compares the 2 object’s bodyType, if any of these are different it will return false. When you add a key to a map, it will check to see if there’s already a key in a map that will return true when compare to the key you want to add, but since you’re adding 2 elements with the same name but different bodyType the comparison will return false, which allows you to add both of them to the map.
My apology for the wordy and lengthy explanation, I hope it helps!
public static final class Key {
private String name;
private BodyTypes bodyType;
private Key(String name, BodyTypes bodyType) {
this.name = name;
this.bodyType = bodyType;
}
public String getName() {
return name;
}
public BodyTypes getBodyType() {
return bodyType;
}
@Override
public int hashCode() {
return this.name.hashCode() + 57 + this.bodyType.hashCode();
}
@Override
public boolean equals(Object obj) {
Key key = (Key) obj;
if(this.name.equals(key.getName())) {
return(this.bodyType == key.getBodyType());
}
return false;
}
@Override
public String toString() {
return this.name + ": " + this.bodyType;
}
}
In this case key class has 2 fields, name and bodyType so when 2 so hashCode and equality depends on those 2 fields.
If you decide to go with string then you can either use name only or name + bodyType.toString() and the 2nd one is not a good idea, since you have to create another string and play around with concatenation it gets very messy quickly, why?
Well if you later decide you need another field then converting everything to string is not a good practice. It is easier to separate that logic into class like this Key class, that way you can decide what fields do you want to use for keys and hashing.
In oop you have classes so having class that just for example is used as key is good practice, having class that transfers data from server to client is good practice etc.