Intro to Objects and References.pdf
Document Details
Tags
Full Transcript
Intro to Objects and References From the OOP, Objects Continued, and More on Objects slides. Contents 1. C and Procedural Programming Paradigm 2. Object-Oriented Programming Paradigm 3. OOP Terms 4. Creating a Car Class 5. Constructors 6. Using a Car Object 7. Static vs Non-Static Members 8...
Intro to Objects and References From the OOP, Objects Continued, and More on Objects slides. Contents 1. C and Procedural Programming Paradigm 2. Object-Oriented Programming Paradigm 3. OOP Terms 4. Creating a Car Class 5. Constructors 6. Using a Car Object 7. Static vs Non-Static Members 8. Objects in Memory 9. Passing Primitives and Objects to Methods 10. Modifying Local Primitive Variables vs Fields 11. Printing Objects 12. Separating Functionality: The Tire Class 13. Preventing Unauthorized Access: public vs private 14. Side Note: this Keyword 15. Practice Problems C and Procedural Programming Paradigm The procedural programming paradigm is where you create procedures (or functions) that call each other. In C, we would create functions that operate on some data. This data would typically be represented as a data type like a struct. The data would be completely separate from the functions that operate on it. Therefore, we would have to pass the data in as an argument to a function. For example, think of performing a phone call. In C, a phone would be declared within some struct phone containing some members like the phoneNumber and contactList. To perform a phone call, you would need to perform a function like performCall(phone, numberToCall). This way of thinking makes it seem like the performCall() function is completely separated from a phone. Object-Oriented Programming Paradigm In OOP, we bundle the data and the methods that operate on that data into an object. The shift into object-oriented programming is based on how we view items in the real world. In the natural world, we would expect the performCall() function to be integrated into the phone. In other words, it would make more sense if we say phone.performCall(numberToCall). So, in the real world, we would see a phone containing some data and some methods built into it. This is the fundamental idea of how object-oriented programming works. OOP Terms Below is a summary of some object oriented terms. Class: A template for an object that bundles related data (fields) and methods that operate on that data. Classes are the blueprints for creating an object. Object: An object is an instance of a class. This is what you built from the blueprint provided by a class. Constructor: A special method that creates a new object in memory. Static: Members that relate to the class. It does not belong to a single instance of the class, but to the class itself. It is shared among all instances of a class. Non-Static: Members that relate to the instance. It belongs to a single instance of the class, and can vary among the instances of a class. Access Modifier: A keyword that determines who can view the members of a class. Some access modifiers in Java include public and private (there are more that we will talk about later). Classes Think of how we build houses. We would usually start with some blueprint, and then construct the house based on that blueprint. We can build multiple houses based on that same blueprint. The interior of each house can be different, but the general structure or exterior is based on that blueprint. Classes are blueprints for an object that contains related fields and methods. In other words, classes are the blueprints for creating an object. When we create a class, we are essentially creating a new data type. For example, let’s analyze a familiar class: String public class ObjectIntro { public static void main(String args[]) { String str1 = "abc"; String str2 = "123"; } } We can see that we are using the String class as the data type for str1 and str2. Both str1 and str2 are objects. Equivalently, str1 and str2 are instances of the String class. These two objects are separate from each other. We created two String objects from the same “blueprint”. Like building a house, we have a general structure for a String, but the interiors can differ. Both objects are Strings, but str1 holds “abc”, but str2 holds “123”. By creating a String object, we can operate on the data it holds via some methods. These methods belong to the String class, which any String object can use. public class ObjectIntro { public static void main(String args[]) { String str = "abc"; int bIndex = str.indexOf('b'); int length = str.length(); } } We call indexOf and length() methods, which are defined in the String class. str can call those methods. In general, an object can access its members via the dot operator. Creating a Car Class Let’s create a basic class that will provide us a template for making a car. We will continue to build upon this class as we explore the new concepts. In general, we can say a car has the following characteristics: Make (Nissan, Toyota, Ford, etc) Model (Altima, Camry, Fusion, etc) Year Color currentFuelAmount Miles Per Gallon We can say that a car has some behaviors Drive some distance (in miles) Rev Engine So, putting this into a class, we have the following: public class Car { public static String make; public static String model; public static String year; public static String color; public static int currentFuelAmount; public static final int MILES_PER_GALLON = 22; public static void drive(int miles) { currentFuelAmount -= (miles / MILES_PER_GALLON); } public static void revEngine() { System.out.println("VROOM VROOM"); } } Using a Car Object So we created our Car class, which will be a blueprint for creating any number of Car objects. Let’s try using our methods by creating a Car object. public class Car { public static String make; public static String model; public static String year; public static String color; public static int currentFuelAmount; public static int milesPerGallon; public static void main(String[] args) { Car myCar = new Car(); myCar.make = "Nissan"; myCar.model = "Altima"; myCar.year = "2024"; myCar.color = "White"; myCar.currentFuelAmount = 200; myCar.milesPerGallon = 22; myCar.drive(22); myCar.revEngine(); } public static void drive(int miles) { currentFuelAmount -= (miles / milesPerGallon); } public static void revEngine() { System.out.println("VROOM VROOM"); } } We create a new Car object using the new keyword. This is how we construct an object. We can set the fields like make, model, year, etc to some initial values. We can also call the drive() and revEngine() methods on the car. Constructors In the above example, we initialized all of the fields inside of main. This causes unnecessary code bloat inside of main. We first create the Car object with the new keyword, then initialize each field line by line. It would be nice to initialize those fields when we create the Car object. We use constructors to create an object. A constructor is a special method with the same name as the class. It has no return type. In the above code, we call the default constructor with the new keyword like so: new Car(). By default, every class you create has a default constructor which you can call. You can create your own constructor to let users of your class initialize fields to some value. For example, let’s make a constructor that allows the user to set the make, model, year, and color of the car when they create the Car object. public static void main(String[] args) { Car myCar = new Car("Nissan", "Altima", "2021", "White"); } // Car Constructor that initializes make, model, year, and color public Car(String _make, String _model, String _year, String _color) { make = _make; model = _model; year = _year; color = _color; } Important Note: If you define your own constructor(s), Java will not create a default constructor for you. In the above example, if you tried using the default Car() constructor, Java will give you a compile time error. We can define multiple constructors for an object. For example, let’s say if the user wants to use whatever default values we set, we can add the following: public static void main(String[] args) { Car myCar = new Car(); } public Car() { make = "Toyota"; model = "Camry"; year = "2024"; color = "Grey"; } // Car Constructor that initializes make, model, year, and color public Car(String _make, String _model, String _year, String _color) { make = _make; model = _model; year = _year; color = _color; } Above, we have two different constructors that a user can call: one with no arguments that sets the fields to some default value, or one where they can specify the make, model, year, and color. Static vs. Non-Static Members So we now have multiple ways of creating a Car. Let’s try making two cars with different values with the current version of our code. Then we can try printing the two different cars. public static String make; public static String model; public static String year; public static String color; public static int currentFuelAmount; public static int milesPerGallon; public static void main(String[] args) { Car defaultCar = new Car(); Car myCar = new Car("Nissan", "Altima", "2021", "White"); defaultCar.printDetails("Default Car"); myCar.printDetails("My Car"); } public Car() { make = "Toyota"; model = "Camry"; year = "2024"; color = "Grey"; } // Car Constructor that initializes make, model, year, and color public Car(String _make, String _model, String _year, String _color) { make = _make; model = _model; year = _year; color = _color; } public static void printDetails(String name) { System.out.print(name + ": "); System.out.print(make + " "); System.out.print(model + " "); System.out.print(year + " "); System.out.print(color + " "); System.out.println(); } Default Car: Nissan Altima 2021 White My Car: Nissan Altima 2021 White But wait, our default car was supposed to be a Toyota Camry 2024 Grey. Why is it the same as myCar? The answer is based on the idea of static vs non-static. A static member belongs to the class. It is the same across all instances of a class. For example, in terms of a car, we can say that every car, regardless of its specific implementation, has 4 tires. Therefore, an appropriate way to represent that is by defining public static int numTires = 4. If we change this value for one car, we change it for all cars. public static int numTires = 4; public static void main(String[] args) { Car defaultCar = new Car(); Car myCar = new Car("Nissan", "Altima", "2021", "White"); // By changing the tires of this car, we are also changing // myCar's numTires to 3 defaultCar.numTires = 3; } Since static members belong to a class, not an instance, we can actually refer to the member via the class itself. For example, we can change the numTires field by calling Car.numTires = 3; A nonstatic member (or instance member) belongs to the object itself. It can vary between instances of a class.To define a nonstatic member, omit the static keyword. In terms of a car, we can say that this specific car has some characteristics that are not common to EVERY car. For example, the make, model, year, and color of a car can vary between cars. By changing one car’s data, we do NOT change it for ALL cars. public String make; public String model; public String year; public String color; public static void main(String[] args) { Car defaultCar = new Car(); Car myCar = new Car("Nissan", "Altima", "2021", "White"); // By changing defaultCar's color to red, we are // NOT changing myCar's color to red too defaultCar.color = "Red"; } Mixing Static and Nonstatic Members Static members can be used by any non-static methods, since every object shares static members. However, you cannot use non-static members by static methods since objects do not share static members. We can see this with the main method: public class StaticTest { public static int staticInt = 4; public int nonStaticInt = 5; public static void main(String args[]) { // Since staticInt is static, main can access it // since it is the same across all instances staticInt = 1; // Since nonStaticInt is nonstatic, main cannot access it // since it can differ between instances nonStaticInt = 2; } } The main method is static, so it can only interact with static members. Therefore, staticInt can be accessed inside of main, but nonStaticInt cannot. The idea behind this is that you do not want some method that is the same across all instances to be able to work with a member of a single instance. A static method should only handle members that relate to the class, not just a single instance. If you want to work with a member of a single instance, you should create a non-static method. In terms of a house, a static method only handles stuff involving the blueprint. It cannot handle stuff involving a specific house. Let’s go through an example of mixing between static and non-static members: public class StaticTest { public static int staticInt = 4; public int nonStaticInt = 5; public void foo() { staticInt = 5; } public static void bar() { nonStaticInt = 4; } } Let’s first define our static and non-static members: ○ Static: staticInt, bar() ○ Non-static: nonStaticInt, foo() foo(): ○ staticInt is a static field, which is allowed inside a non-static method bar(): ○ nonStaticInt is a non-static field, which is NOT allowed inside a static method Rewriting the Car Class with Static and Non-Static Members So after learning about static and non-static, let’s apply it to our car class. We should define what is the same across all cars (static) and what can vary between cars (non- static): Same Across All Cars (Static): ○ numTires Can Vary Between Cars (Non-Static) ○ Make, model, year, color, currentFuelAmount, milesPerGallon public class Car { public String make; public String model; public String year; public String color; public int currentFuelAmount; public int milesPerGallon; public static int numTires = 4; public static void main(String[] args) { Car defaultCar = new Car(); Car myCar = new Car("Nissan", "Altima", "2021", "White"); defaultCar.printDetails("Default Car"); myCar.printDetails("My Car"); } public Car() { make = "Toyota"; model = "Camry"; year = "2024"; color = "Grey"; } // Car Constructor that initializes make, model, year, and color public Car(String _make, String _model, String _year, String _color) { make = _make; model = _model; year = _year; color = _color; } public void printDetails(String name) { System.out.print(name + ": "); System.out.print(make + " "); System.out.print(model + " "); System.out.print(year + " "); System.out.print(color + " "); System.out.println(); } public void drive(int miles) { currentFuelAmount -= (miles / milesPerGallon); } public void insertFuel(int amount) { currentFuelAmount += amount; } public void revEngine() { System.out.println("VROOM VROOM"); } } Default Car: Toyota Camry 2024 Grey My Car: Nissan Altima 2021 White Now we have two cars with varying make, model, year, and color! Note: You might say that drive() or insertFuel() should be static since every car can drive and insert fuel. However, remember that the fields (like currentFuelAmount) are non-static, which means they cannot be used inside a static method. That is why each of those methods are defined as non-static. You can think of it as drive() and insertFuel() depend on circumstances (fuel) relating to the specific car Objects in Memory Stack vs Heap Space Stack Space is where local variables reside. The variables live and die within the method they reside in. Any variable declared inside a method will have their memory freed after the method completes its execution. Heap Space is where objects reside. Anything in heap space can live beyond the life of the method that created it. What is Stored in an Object Variable? When you create an object, the object itself is stored in heap space. The variable you create to hold that object actually stores a reference to the object residing in heap space. For example, let’s look at what happens when we create a car object: public static void main(String[] args) { Car defaultCar = new Car(); } STACK SPACE: main() +---------------+ | | | defaultCar | | +---------+ | | | 0x439 | | | +---------+ | | | +---------------+ HEAP SPACE: 0x439 +---------------------------------------------------+ | | | make model year color | | +--------+ +---------+ +--------+ +--------+ | | | Toyota | | Camry | | 2024 | | Grey | | | +--------+ +---------+ +--------+ +--------+ | | | +---------------------------------------------------+ In the above code, we created a car in main(). This creates a new car object that resides in heap space. The variable defaultCar stores the reference (or address) to that object in heap space. Stack space variables live and die within the method that they reside in. For example, let’s say we have the following code: public static void main(String[] args) { createCar() } public static void createCar() { Car defaultCar = new Car(); } We start at the main() method: STACK SPACE: main() +---------------+ | | | | | | +---------------+ HEAP SPACE: Nothing... Then we enter createCar(). Here’s what memory looks like after creating the default car, but before we completely exit createCar(): STACK SPACE: main() createCar() +---------------+ +---------------+ | | | | | | | defaultCar | | | | +---------+ | +---------------+ | | 0x439 | | | +---------+ | +---------------+ HEAP SPACE: 0x439 +---------------------------------------------------+ | | | make model year color | | +--------+ +---------+ +--------+ +--------+ | | | Toyota | | Camry | | 2024 | | Grey | | | +--------+ +---------+ +--------+ +--------+ | | | +---------------------------------------------------+ Here’s what memory looks like after we exit createCar(): STACK SPACE: main() +---------------+ | | | | | | +---------------+ HEAP SPACE: 0x439 +---------------------------------------------------+ | | | make model year color | | +--------+ +---------+ +--------+ +--------+ | | | Toyota | | Camry | | 2024 | | Grey | | | +--------+ +---------+ +--------+ +--------+ | | | +---------------------------------------------------+ Garbage Collection So we created this car object. The variable containing the reference to the object (defaultCar) died after createCar() terminated. However, the object still exists in heap space. This is problematic, since we now have no more references to the car object we created. We have created a memory leak. We have no way of accessing that object anymore. However, we do not need to worry about memory leaks in Java due to the Garbage Collector. Java has a mechanism called the Garbage Collector that frees up unused memory inside of heap space automatically for us. Whenever the garbage collector runs, it scans all memory pieces created during a program’s execution and sees if there is a reference still available. If there is no reference pointing to that object, the Garbage Collector will free that memory. A few notes about Garbage Collection: The Garbage Collector cannot be called by the user. It runs when it wants in the background of our running process. When we exit the main() method, all system resources are reallocated back to the machine (or in Java’s case, the JVM) What Happens When Assigning an Object to a Variable? If you remember from the Strings slides, we saw that Strings are immutable. This is true for all objects. Because the variable stores a reference, we are overwriting our reference to the object in memory. We are not changing the values themselves. Looking at the example of Strings: So, if we start by initializing text with “Hello”: String text = "Hello"; Stack Space text 0x239 +---------+ | 0x467 | +---------+ | | Heap Space | 0x467 +-----------+ | "Hello" | +-----------+ …and we then set text to “World”: text = "World"; Stack Space text 0x239 +---------+ | 0x569 | ------------+ +---------+ | | Heap Space | 0x467 | 0x569 +-----------+ +-----------+ | "Hello" | | "World" | +-----------+ +-----------+ Let’s apply this same logic to our Car: public static void main(String[] args) { Car defaultCar = new Car(); Car myCar = new Car(“Nissan”, “Altima”, “2021”, “White”); defaultCar = myCar; } So we create both cars in memory: STACK SPACE: main() +----------------------------+ | | | defaultCar myCar | | +---------+ +---------+ | | | 0x439 | | 0x037 |--|---------------------------+ | +---------+ +---------+ | | | | | | +----------------------------+ | | | | | HEAP SPACE: | | | 0x439←—-+ | +---------------------------------------------------+ | | | | | make model year color | | | +--------+ +---------+ +--------+ +--------+ | | | | Toyota | | Camry | | 2024 | | Grey | | | | +--------+ +---------+ +--------+ +--------+ | | | | | +---------------------------------------------------+ | | 0x037←—--------------------------------------------------+ +---------------------------------------------------+ | | | make model year color | | +--------+ +---------+ +--------+ +--------+ | | | Nissan | | Altima | | 2021 | | White | | | +--------+ +---------+ +--------+ +--------+ | | | +---------------------------------------------------+ Then we assign defaultCar the value of myCar: STACK SPACE: main() +----------------------------+ | | | defaultCar myCar | | +---------+ +---------+ | | | 0x037 | | 0x037 |--|---------------------------+ | +---------+ +---------+ | | | | | | +----------------------------+ | | | +------------------------------------------------+ | HEAP SPACE: | | 0x439 | +---------------------------------------------------+ | | | | | make model year color | | | +--------+ +---------+ +--------+ +--------+ | | | | Toyota | | Camry | | 2024 | | Grey | | | | +--------+ +---------+ +--------+ +--------+ | | | | | +---------------------------------------------------+ | | 0x037←---------------------------------------------------+ +---------------------------------------------------+ | | | make model year color | | +--------+ +---------+ +--------+ +--------+ | | | Nissan | | Altima | | 2021 | | White | | | +--------+ +---------+ +--------+ +--------+ | | | +---------------------------------------------------+ Passing Primitives and Objects to Methods In Java, all methods are pass-by-value. When you pass a variable as an argument to a method, the parameter holds a copy of the value the variable stores. However, keep in mind what the arguments you pass to a method actually store. For primitives, this is straightforward. When you pass in a primitive, the method parameter holds a copy of that value. public class MethodTest { public static void main(String args[]) { int x = 0; foo(x); System.out.println(x); } public static void foo(int x) { x = 1; } } 0 The parameter x in foo() stores the value of 0. You are not updating the x variable you declared in main(). For objects, there is a slight trick. Remember that the object variables actually store a reference to the object. So when we pass in the variable as an argument to a method, the parameter will hold a copy of the reference. public class Car { public String color; public static void main(String args[]) { Car myCar = new Car(); myCar.color = "red"; foo(myCar); System.out.println(myCar.color); } public static void foo(Car car) { car.color = "green"; } } green Since car stores a reference to an object, we can modify the color field of the object itself and have our changes persist. Here’s a memory diagram of what’s happening at the following points in the program: 1) Creating the car and changing the color to red. STACK SPACE: main() +--------------+ | myCar | | +---------+ | | | 0x439 | | | +---------+ | +--------------+ HEAP SPACE: 0x439 +-----------+ | color | | +-----+ | | | red | | | +-----+ | +-----------+ 2) Passing car to foo() and updating the color to green (before foo() terminates) STACK SPACE: main() foo() +--------------+ +---------------+ | myCar | | car | | +---------+ | | +---------+ | | | 0x439 | | | | 0x439 | | | +---------+ | | +---------+ | +--------------+ +---------------+ HEAP SPACE: 0x439 +-------------+ | color | | +-------+ | | | green | | | +-------+ | +-------------+ 3) Returning back to main() STACK SPACE: main() +--------------+ | myCar | | +---------+ | | | 0x439 | | | +---------+ | +--------------+ HEAP SPACE: 0x439 +-------------+ | color | | +-------+ | | | green | | | +-------+ | +-------------+ Modifying Local Primitive Variables vs Fields You can only modify primitive variables locally at the method you declare them at. Passing primitives to methods creates a copy of the value, it does not update the variable itself. public class MethodTest { public static void main(String args[]) { int x = 0; foo(x); System.out.println(x); } public static void foo(int x) { x = 1; } } 0 Fields are local to the class itself. Therefore, you can modify them in any method and the changes will persist. public class MethodTest { public static int x; public static void main(String args[]) { foo(); System.out.println(x); } public static void foo() { x = 1; } } 1 Printing Objects When using any of the printing methods, they will automatically call an object’s toString() method. Every object in Java has a default toString() method that simply prints the reference value of the object. However, most built in classes have a toString() method that you can call implicitly or explicitly. For example, the ArrayList class has a modified toString() method to print out its contents. You can call this method implicitly or explicitly: public static void main(String args[]) { ArrayList arrayList = new ArrayList(); System.out.println(arrayList); // Implicitly calls toString() System.out.println(arrayList.toString()); // Explicitly call toString() } We do not have a toString() method defined for our Car class, so we will just be printing the reference stored in the variable car: public class Car { public String color; public static void main(String args[]) { Car myCar = new Car(); System.out.println(myCar); } } Car@7344699f Separating Functionality: The Tire Class So we have our current car class defined as shown below: public class Car { public String make; public String model; public String year; public String color; public int currentFuelAmount; public int milesPerGallon; public static int numTires = 4; public static void main(String[] args) { Car defaultCar = new Car(); Car myCar = new Car("Nissan", "Altima", "2021", "White"); defaultCar.printDetails("Default Car"); myCar.printDetails("My Car"); } public Car() { make = "Toyota"; model = "Camry"; year = "2024"; color = "Grey"; } // Car Constructor that initializes make, model, year, and color public Car(String _make, String _model, String _year, String _color) { make = _make; model = _model; year = _year; color = _color; } public void printDetails(String name) { System.out.print(name + ": "); System.out.print(make + " "); System.out.print(model + " "); System.out.print(year + " "); System.out.print(color + " "); System.out.println(); } public void drive(int miles) { currentFuelAmount -= (miles / milesPerGallon); } public void insertFuel(int amount) { currentFuelAmount += amount; } public void revEngine() { System.out.println("VROOM VROOM"); } } What if we wanted to store data about the tires of each car. For example, each tire has a current air pressure and a max air pressure. How should we go about adding tires to the Car class? One approach is to directly add the tire data to the car class. For example: public class Car { public String make; public String model; public String year; public String color; public int currentFuelAmount; public int milesPerGallon; // Tires Fields public static int numTires = 4; public int tirePressure1 = 33; public int tirePressure2 = 33; public int tirePressure3 = 32; public int tirePressure4 = 34; public static maxTirePressure1 = 35; public static maxTirePressure2 = 35; public static maxTirePressure3 = 35; public static maxTirePressure4 = 35; public static void main(String[] args) { Car defaultCar = new Car(); defaultCar.setTirePressure1(35); defaultCar.setTirePressure2(34); } // Car Methods... // Tire Methods public void setTirePressure1(int newPressure) { tirePressure1 = newPressure; } public void setTirePressure2(int newPressure) { tirePressure2 = newPressure; } public void setTirePressure3(int newPressure) { tirePressure3 = newPressure; } public void setTirePressure4(int newPressure) { tirePressure4 = newPressure; } } We track each tire’s current and max pressure inside of fields. We then have a method to update each tire with some new pressure. Hopefully, you see some design flaws with the code above. What if cars now hold 5 tires? We would have to create a new field to hold the current pressure, a new field to hold its max pressure, and a new method to set its pressure. What if they revert back to 4 tires after our change? We would have to remove the obsolete fields and methods. What if we want to create a Bike class? Well we would have to copy and paste this functionality. Instead, let’s approach it a different way. In the natural world, a tire is not completely tied to a car. Tires are used for other things, like a bike. It wouldn’t make sense to define the same characteristics of a tire in two separate classes (a car and a bike). We should think of a tire as its own separate thing. In general, when we want to add some separate functionality to a class, we create a new class. This is the main idea behind the Single Responsibility Principle. Every method/class should do or represent one thing. A car does contain tires, but tires do not solely belong to cars. So, let’s create a new Tire class. This will be defined inside a new file Tire.java. Remember that all public classes must be defined inside a.java file of the same class name public class Tire { public int currentAirPressure; public int maxAirPressure; public Tire(int _maxAirPressure) { currentAirPressure = _maxAirPressure; maxAirPressure = _maxAirPressure; } public void setPressure(int newPressure) { currentAirPressure = newPressure; } } Now, let’s see how we can use the Tire class inside of our Car class: public class Car { public String make; public String model; public String year; public String color; public int currentFuelAmount; public int milesPerGallon; // Tires Fields public static int numTires = 4; Tire tire1; Tire tire2; Tire tire3; Tire tire4; public static void main(String[] args) { Car defaultCar = new Car(); defaultCar.tire1.setPressure(33); defaultCar.tire2.setPressure(34); } // Car Methods public Car() { tire1 = new Tire(35); tire2 = new Tire(35); tire3 = new Tire(35); tire4 = new Tire(35); } } In the above code, we created a new car using the default Car() constructor. We updated the car’s first tire to a pressure of 33, and the second tire to a pressure of 34. If we want to add a new tire, we simply just need to add a single field tire5, and initialize it in the constructor. If we want to remove a tire, we simply just remove the field and its initialization. We only needed to update 2 lines of code! We have effectively separated the functionality of a tire from the car. The Car class does not need to worry about the implementation of setPressure(). In other words, we have abstracted the functionality of tires, letting any class (not just Car) use them. For example, in the following Bike class, we can use the Tire class as well! public class Bike { public Tire frontTire; public Tire backTire; // Bike Methods... } Preventing Unauthorized Access: public vs private An access modifier defines who can view/manipulate the class, method, or field. So far, every class, method, and field we have defined has been public. Note: This section only goes over the use of public and private access modifiers. However, there are two other ones that we will talk about later. So with the current version of our Car and Tire classes, every field is public. An access modifier of public means that anyone in any class can access that field. For example, in the Tire class, we have two public fields: currentTirePressure and maxTirePressure. The problem with those two fields being public is that any class can modify that field to whatever they want. For example, the Car class can directly modify currentTirePressure: public class Car { public String make; public String model; public String year; public String color; public int currentFuelAmount; public int milesPerGallon; // Tires Fields public static int numTires = 4; Tire tire1; Tire tire2; Tire tire3; Tire tire4; public static void main(String[] args) { Car defaultCar = new Car(); defaultCar.tire1.currentTirePressure = -33; defaultCar.tire2.currentTirePressure = 50000; } // Car Methods public Car() { tire1 = new Tire(35); tire2 = new Tire(35); tire3 = new Tire(35); tire4 = new Tire(35); } } As you can see, within the Car class, we can set currentAirPressure to a negative value, or a value that exceeds our maxAirPressure. This is extremely dangerous! So how do we prevent unauthorized modification of the currentTirePressure value? We can make those values instead have a private access modifier. The private access modifier means that the field can only be accessed inside of the class it resides in. Let’s convert those two fields into private fields: public class Tire { private int currentAirPressure; private int maxAirPressure; public Tire(int _maxAirPressure) { currentAirPressure = _maxAirPressure; maxAirPressure = _maxAirPressure; } public void setPressure(int newPressure) { currentAirPressure = newPressure; } } Now if we try to access either of those variables directly within the Car class, we will get a compilation error. As you can see, we can still use the two private fields inside of the Tire class, but cannot use the two private fields inside of the Car class. Therefore, we are preventing unauthorized modification of those two variables. Then the question becomes how do we access those fields from outside classes? It would be good to let users see the values of currentAirPressure and maxAirPressure, but prevent them from modifying it in certain ways.That is where getters and setters come into play. Getters Getters are public methods that simply return the private field’s value.This allows the user to get the value of the field without giving them direct access to that field. public class Tire { private int currentAirPressure; private int maxAirPressure; public Tire(int _maxAirPressure) { currentAirPressure = _maxAirPressure; maxAirPressure = _maxAirPressure; } public void getCurrentPressure() { return currentAirPressure; } public void getMaxPressure() { return maxAirPressure; } } Setters Setters are public methods that allow “supervised” modification of private fields. We already have the defined setPressure() method. However, we can now put up some safeguards to prevent the user from setting the currentAirPressure to some invalid value: public class Tire { private int currentAirPressure; private int maxAirPressure; public Tire(int _maxAirPressure) { currentAirPressure = _maxAirPressure; maxAirPressure = _maxAirPressure; } public void setPressure(int newPressure) { if (newPressure > 0 && newPressure 0 && newPressure