Java 11 Developer Certification - Encapsulation
December 28, 2020
What we are covering in this lesson
- What is Encapsulation
- Access Modifiers
- Overriding
- Access Modifiers usage in Constructors
- Applying Encapsulation principles to a class
What is Encapsulation
Encapsulation, as we already know, is one of the core concepts of Object Oriented Programming. It has mainly 2 aspects
- information (data) hiding
- bundling data and behaviour into a single unit
Encapsulation helps in providing only the required or needed information to the outside world, and not the details of the internal mechanisms. This helps in protecting the integrity of data in the object, preventing abuses and alterations from the outside world. It also allows the implementation details to change, without affecting the consumers upstream of the object. As long as the contract that was published doesn’t change, an application should not have to change, even if the object’s implementation does.
In Object Oriented Design, we avoid tight coupling of classes, which means we try to make them as independent of each other as possible. When we bundle data and behaviour into a single class, we avoid coupling. Another way to avoid coupling is to prevent classes from directly accessing another class’s instance variables.
A well encapsulated object is considered one that doesn’t expose it’s fields publicly to other classes, but allows access via methods with appropriate access modifiers. So, this allows the encapsulated object to change all the aspects of a particular field, as well as to protect the data in it.
Access Modifiers
This is something we have already covered in the previous sections, so lets quickly revise the access modifiers.
We have 4 access modifiers, and the below table shows all the modifiers in the order of least restrictive to most restrictive.
Access modifier | Description | Available to classes in any package | Available to classes in same package | Available to subclasses |
---|---|---|---|---|
public | Allows full access to member, regardless of package or hierarchy. Any class in any package can access this type of member | Yes | Yes | Yes |
protected | Allows access to any class in the same package and subclass of the class, regardless of the package it is in | No | Yes | Yes |
default | This will be applicable when no access modifier is specified. The allowed access is package (package-private) or default access. It allows the member to be accessed by any other class in the same package | No | Yes | No |
private | Using the private modifier prevents any class, other than the one the member is declared in, from accessing the member. Note that the nested class can access the outer class’s private members | No | No | No |
Unlike methods, fields in a subclass hide those of the same name in the superclass.
package maxCode.online.encapsulation;
class PackageAClass {
void testAccess() {
SuperClass c = new SuperClass();
System.out.println(
"A non-subclass class has access to all but " + "private fields of another class in same package");
System.out.println("- SuperClass().publicInt = " + c.publicInt);
System.out.println("- SuperClass().packageInt = " + c.packageInt);
System.out.println("- SuperClass().protectedInt = " + c.protectedInt);
//System.out.println("- SuperClass().privateInt = " + c.privateInt); //No access
}
}
public class SuperClass {
public int publicInt = 10;
int packageInt = 20;
protected int protectedInt = 30;
private int privateInt = 40;
public static void main(String[] args) {
new SuperClass().testAccess();
new PackageAClass().testAccess();
}
void testAccess() {
System.out.println(this.getClass().getName() + " has access to all of its own attributes:");
System.out.println("- this.publicInt = " + this.publicInt);
System.out.println("- this.packageInt = " + this.packageInt);
System.out.println("- this.protectedInt = " + this.protectedInt);
System.out.println("- this.privateInt = " + this.privateInt);
}
}
Output:
maxCode.online.encapsulation.SuperClass has access to all of its own attributes:
- this.publicInt = 10
- this.packageInt = 20
- this.protectedInt = 30
- this.privateInt = 40
A non-subclass class has access to all but private fields of another class in same package
- SuperClass().publicInt = 10
- SuperClass().packageInt = 20
- SuperClass().protectedInt = 30
As we can see, the SuperClass has access to all its members regardless of the access specifier. The subclass has access to some of the members, except the private member of the SuperClass (commented out one).
Lets look at another such example digging deeper into the access modifiers, and classes spanning different packages.
package maxCode.online.encapsulation2;
import maxCode.online.encapsulation.SuperClass;
class PackageBClass {
void testAccess() {
SuperClass c = new SuperClass();
System.out.println("A non sub-class class in a different package"
+ " has access only to a public class's public fields ");
System.out.println("- SuperClass().publicInt = " + c.publicInt);
/*System.out.println("- SuperClass().packageInt = " + c.packageInt);
System.out.println("- SuperClass().privateInt = " + c.privateInt);
System.out.println("- SuperClass().protectedInt = " + c.protectedInt);*/
}
}
public class ParentClass extends SuperClass {
public static void main(String[] args) {
new PackageBClass().testAccess();
new ParentClass().testAccess();
}
void testAccess() {
System.out.println(this.getClass().getName() + " has access to only it's parent's public and protected"
+ " attributes if it's declared in another package:");
System.out.println("- this.publicInt = " + this.publicInt);
System.out.println("- this.protectedInt = " + this.protectedInt);
// You can use the word super to get at it too, but superfluous in this context
System.out.println("- super.protectedInt = " + super.protectedInt);
// Access protected variable through another instance of ParentClass
System.out.println("- new ParentClass().protectedInt = " + new ParentClass().protectedInt);
// What happens if you access through instance of SuperClass here?
// System.out.println("- new SuperClass().protectedInt = " + new SuperClass().protectedInt);
// What happens if you access through instance of SuperClass here?
System.out.println("- new SuperClass().protectedInt not available" + " through SuperClass instance in "
+ this.getClass().getName());
}
}
Output
A non sub-class class in a different package has access only to a public class's public fields
- SuperClass().publicInt = 10
maxCode.online.encapsulation2.ParentClass has access to only it's parent's public and protected attributes if it's declared in another package:
- this.publicInt = 10
- this.protectedInt = 30
- super.protectedInt = 30
- new ParentClass().protectedInt = 30
- new SuperClass().protectedInt not available through SuperClass instance in maxCode.online.encapsulation2.ParentClass
The ParentClass extends SuperClass which is another package. The ParentClass has a main method and testAccess method to print out the access levels we have to various fields.
protectedInt can be access using this
as well as super
.
A non sub-class class in a different package has access only to a public class’s public fields.
Different package ParentClass has access to only it’s parent’s public and protected attributes if it’s declared in another package.
We are also able to access protectedInt by creating a new instance of ParentClass. But the same fails if we try to access protectedInt using an instance of SuperClass, due to classes in separate packages.
Overriding
Overriding is actually a part of inheritance, but we will explore a bit of it with respect to the access modifiers. There are different rules to which access modifiers are allowable based on the inherited elements. You can override a parent’s attribute, creating an attribute on the child class, with the same name and type as the parent. This is called attribute hiding because you are hiding the parent’s attribute with the child’s. A child’s attribute can be more restrictive or less restrictive than the parent’s. There are no invalid combinations.
package maxCode.online.encapsulation3;
//Create a base class Animal which has one attribute with each
//type of access modifier
class Animal {
// Each attribute has a different access modifier
public String name = "Unknown";
protected String breed = "Unknown";
String owner = "Unknown";
private String type = "Unknown";
public String toString() {
return owner + "'s " + type + " " + name + " is a " + breed;
}
}
// Dog declares exact same attributes as its parent, Animal, but modifies
// the access modifier of each attribute
class Dog extends Animal {
// type was private on Animal, we declare it public here
// Less restrictive hiding
public String type = "Dog";
// owner was package-private on Animal, we declare it protected here
// Less restrictive hiding
protected String owner;
// breed was protected on Animal, we declare it package-private by default
// package-private is more restrictive hiding than protected
String breed;
// name was public on Animal, we declare it private here
// More restrictive hiding
private String name;
// Constructor to make creating a Dog easy
Dog(String owner, String name, String breed) {
this.owner = owner;
this.name = name;
this.breed = breed;
}
public String toString() {
return super.toString();
}
}
public class AllowableChildModifiers {
public static void main(String[] args) {
Animal a = new Animal();
System.out.println("Created a generic animal with defaults:");
System.out.println(a.toString());
System.out.println("Created a specific dog");
Dog pug = new Dog("Fran", "Brandy", "Pug");
System.out.println(pug.toString());
System.out.println("Pug's type is " + pug.type);
System.out.println("Pug's breed is " + pug.breed);
System.out.println("Pug's owner is " + pug.owner);
//System.out.println("Pug's name is " + pug.name); //The field Dog.name is not visible
a = pug;
System.out.println("Pug's breed is " + a.breed);
System.out.println("Pug's owner is " + a.owner);
System.out.println("Pug's name is " + a.name);
//System.out.println("Pug's type is " + a.type); //The field Animal.type is not visible
}
}
Output
Created a generic animal with defaults:
Unknown's Unknown Unknown is a Unknown
Created a specific dog
Unknown's Unknown Unknown is a Unknown
Pug's type is Dog
Pug's breed is Pug
Pug's owner is Fran
Pug's breed is Unknown
Pug's owner is Unknown
Pug's name is Unknown
We have Animal class with 4 attributes, each with a different access modifier, and a toString method. Then we have a Dog class which extends the Animal class, and has the same 4 attributes but with different access modifiers which are either more or less restrictive hiding. We also have a constructor there and a toString method.
The main class, AllowableChildModifiers, creates new instances of both Animal and Dog class and prints out the results.
We can clearly see the attributes which are marked private
are not accessible directly.
The toString method for Dog calls the toString of parent Animal which doesnt have any knowledge of the values defined in child class, and so we see default value (Unknown) getting printed.
Even after we assign pug to a (a=pug), we are able to access the variable values except for the type as it is declared private in Animal.
package maxCode.online.encapsulation3;
//Employee class has a method for each type of modifier...
class Employee {
public void printPublic() {
System.out.println("An Employee can make a public statement");
}
protected void printProtected() {
System.out.println("An Employee can make a protected statement");
}
void printPackage() {
System.out.println("An Employee can make a package statement");
}
private void printPrivate() {
System.out.println("An Employee can make a private statement, "
+ "but only the employee can make it accessible through other means..");
}
// blog method is pass through to the private printPrivate method
protected void blog() {
printPrivate();
}
}
// subclass of Employee overrides some of Boss's methods,
// changing access modifiers
class Boss extends Employee {
// making a private method on parent public on child
public void printPrivate() {
System.out.println("A Boss's private statements can be made public now");
}
// making a package-private method on parent protected on child
protected void printPackage() {
System.out.println("A Boss's packaged speech can be shared with his children as well now.");
}
// private void printPublic() {
// System.out.println("A Boss's public speech cannot be made private");
// }
//
// void printProtected() {
// System.out.println("A Boss's protected speech cannot be packaged");
// }
}
public class AllowableOverrideModifiers {
public static void main(String[] args) {
Employee e = new Employee();
e.printPublic();
e.printProtected();
e.printPackage();
// Employee use's blog method to provide controlled
// access to its printPrivate method
e.blog();
Boss b = new Boss();
b.printPackage();
b.printPrivate();
}
}
Output
An Employee can make a public statement
An Employee can make a protected statement
An Employee can make a package statement
An Employee can make a private statement, but only the employee can make it accessible through other means..
A Boss's packaged speech can be shared with his children as well now.
A Boss's private statements can be made public now
In the above code, we have an Employee class with 4 methods, each method with a different access modifier. Another method blog() has protected access and calls the private method.
Boss class entends the Employee class and extends 2 methods from Employee class, and makes private method public and default method as protected, making the methods less restrictive.
Then we have the main class, instantiating the objects there and calling then methods.
The commented out methods in Boss class would otherwise have thrown compiler error. The problem here is that you can’t make methods more restrictive on the child or subclass. Extending a class implies you’re gonna be adding functionality to it. By trying to restrict access on methods, you are attempting the opposite, limiting the functionality of a parent class, which is not permissible. The parent class created a contract at a public and or package or protected level, and any class extending it cannot disregard or override that contract.
So the example we just saw demonstrated two examples where you were restricted from using one of the four access modifiers. So the below chart tries to show you all the cases where a particular access modifier is not allowed.
Element | public | {no modifier} | default | private | what no modifier means |
---|---|---|---|---|---|
Top Level Types (Class, Interface, Enum) | Not Allowed | Not Allowed | package | ||
overriding public method of a parent class | Not Allowed | Not Allowed | Not Allowed | package | |
overriding protected method of a parent class | Not Allowed | Not Allowed | package | ||
overriding package method of a parent class | Not Allowed | package | |||
Abstract Methods | Not Allowed | package | |||
Interface Methods | Not Allowed | method must be implemented if private used | public (as of Java 8) | ||
Interface Attributes | Not Allowed | Not Allowed | public (as of Java 8) | ||
Enum constants (defaults to public) | Not Allowed | Not Allowed | Not Allowed | public | |
Enum constructors | Not Allowed | Not Allowed | redundant if used | private |
Now we will be looking at the constructor access levels and how these accesses work across classes which span through multiple packages.
Access Modifiers usage in Constructors
Below we have 2 classes in separate packages, and we will go through how the constructors of these classes can be accessed when the class are in different packages.
package maxCode.online.encapsulation;
class LevelOneClass {
// Protected access constructor
protected LevelOneClass() {
System.out.println("protected LevelOneClass " + "no args constructor");
}
// Package-private access constructor
LevelOneClass(String text) {
System.out.println("package level LevelOneClass " + "single params constructor");
}
}
public class LevelTwoClass extends LevelOneClass {
// Protected access constructor
protected LevelTwoClass() {
System.out.println("protected LevelTwoClass " + "no args constructor");
}
// Package-private access constructor
LevelTwoClass(String text) {
System.out.println("package level LevelTwoClass " + "single params constructor");
}
}
package maxCode.online.encapsulation2;
import maxCode.online.encapsulation.LevelTwoClass;
//Extends class from a different package
class LevelThreeClass extends LevelTwoClass {
// package-private constructor
LevelThreeClass() {
this("good");
System.out.println("package LevelThreeClass " + "no args constructor");
}
// private constructor
private LevelThreeClass(String text) {
super();
System.out.println("private LevelThreeClass " + "single params constructor");
}
}
public class ConstructorAccess {
public static void main(String[] args) {
// The constructor test
new LevelThreeClass();
}
}
Output
protected LevelOneClass no args constructor
protected LevelTwoClass no args constructor
private LevelThreeClass single params constructor
package LevelThreeClass no args constructor
LevelOne class has a protected as well as a package private constructor (default). LevelTwo class extends LevelOne and also has a protected and package private constructor. Then we have another class ConstructorAccess which is in a different package, and instantiates the LevelThree class object. LevelThree class extends LevelTwo class and has a private and package private constructor.
If in the LevelThree class constructor we make a call to any package private constructor of LevelTwo class, e.g. super("Calling package private constructor");
, we will get a compiler error stating The constructor LevelTwoClass(String) is not visible
.
So such constructors cannot be accessed from outside that package.
Removing the protected modifier on LevelTwo class makes it a package private constructor. The constructor is not available to LevelThree class even if it extends LevelTwo class, because both classes are in different packages.
Applying Encapsulation principles to a class
We already know that encapsulation means information hiding. We saw in previous codes how Java provides different ways to hide class members. We can hide a member from every consumer - private, from consumers in other packages - no modifier package, or we can hide members from other packages but still provide access to classes in the inheritance tree - protected.
There are two objectives involed in hiding information.
- The first objective is to protect the data of a class from unintended or unwelcome changes.
- The second objective is to protect upstream consumers of the class from unintended consequences causing a minimum of disruption, should be the class need to change.
That is why all class and instance variables should be declared private with access to the data strictly controlled through methods which we can examine and verify the requests being made. This is the convention used by a special type of class called a bean.
A bean is a Java class which follows certain programming conventions that follow the JavaBeans guidelines, allowing applications and tools to figure out the bean’s properties, methods, and events. We will be looking at how to define the property in a bean class, and supplying public getter setter methods.
A getter method (accessor method) starts with the prefix get followed by a property name with camel case and returns an element of the property’s type. This retrieves the property’s data from the bean, e.g., getFirstName will get the value of firstName from the object.
A setter method starts with the prefix set followed by the property name with camel case and takes as its argument, a value that the property can be set to. The method generally doesn’t return a value.
A special case for boolean properties allows the accessor method to be defined using prefix is instead of get. So the accessor for a boolean property first would be isFirst();.
A property is usually an attribute on the class with the same name in the setter getter methods, but this isn’t a requirement. A property could be any attribute or a code block of some sort.
In IDEs, we do have an option to generate constructors and getter/setters automatically. For Eclipse IDE, right click on the code editor, select Source -> Generate Getters and Setters. Also, Source -> Generate Constructor using fields.
package maxCode.online.encapsulation;
public class FirstBean {
private String name;
private int age;
private boolean first;
private int myOwnVariable;
public int getFieldAlias() {
return myOwnVariable;
}
public void setFieldAlias(int fieldAlias) {
this.myOwnVariable = fieldAlias;
}
public boolean isFirst() {
return first;
}
public void setFirst(boolean first) {
this.first = first;
}
public FirstBean(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public static void main(String[] args) {
}
}
The Java bean is one way to implement encapsulation in Java, and a standard, but it is not the only way. So for encapsulation, always remember that encapsulation
- combines data and behaviour into a single class
- hides and protects attributed by making them private
-
provides a mechanism that sets attribute or attributes.
- This mechanism could be a public constructor, a public setter method or some other mechanism.
- If the attribute is a variable reference, you should remember that the underlying object passed of the reference, setting it, could change it.
- Making a copy of the object referenced is a way to ensure the data doesn’t change.
-
provides a mechanism for getting data from the attribute.
- This mechanism could be a standard getter or a public method with any kind of name that returns the data.
- Watch out for methods that return variable references directly remembering that the underlying object could be ordered by the calling code.
Implementing encapsulation may not always be as straight forward as it looks, specially when we use reference variables. Lets look at the code below and try to understand encapsulation implementation in it.
package maxCode.online.encapsulation;
//Person 'Bean'
public class Person {
// We declare some private attributes
private String name;
private StringBuilder address;
private int age;
// We have a constructor for easy creation of Person and
// population of its attributes
public Person(String name, StringBuilder address, int age) {
this.name = name;
this.address = address;
//this.address = new StringBuilder(address);
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public StringBuilder getAddress() {
return address;
}
public void setAddress(StringBuilder address) {
this.address = address;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
// Customize toString method
public String toString() {
return "Person{" + "name='" + name + '\'' + ", address=" + address + ", age=" + age + '}';
}
}
package maxCode.online.encapsulation;
public class PersonTester {
public static void main(String[] args) {
// Create some local variables
StringBuilder address = new StringBuilder("2234 Maple Ave, Ralphtown, PA, 19000");
int age = 45;
String name = "Ralph";
// Create instance of person with local variable references
Person p = new Person(name, address, age);
// Change the local variable's data
address.append("-0005");
name = "Ralph's Wife";
age = 40;
// Create another instance of person with local variable references
Person p2 = new Person(name, address, age);
System.out.println(p);
System.out.println(p2);
}
}
Output
Person{name='Ralph', address=2234 Maple Ave, Ralphtown, PA, 19000, age=45}
Person{name='Ralph's Wife', address=2234 Maple Ave, Ralphtown, PA, 19000-0005, age=40}
We have a well encapsulated class Person, with attributes marked private and all getters and setters with one constructor. Next we have a PersonTester class (in the same package). We create two instances of Person here.
The output is quite interesting as we are updating the address for second instance, but the output shows both addresses updated. That is because if your attributes are reference variables, you should actually make a copy of the data in the bean before assigning it to the instance variables.
So to correct this, we should be doing this.address = new StringBuilder(address);
in the Person constructor. On doing this, the address is updated only for the second instance.
This is how we should be using the reference variables in case of encapsulation for correct data.
Thats all for encapsulation, see you in the next section!