1. Introduction
Encapsulation is an important property of object-oriented programming and many design patterns. To efficiently protect encapsulation, the mechanisms of ownership and immutability are needed. However, when it comes to encapsulation in Java, the problem of representation exposure is difficult to avoid. The problem will cause programs written in Java to be less predictable. The term “representation exposure” was proposed by Noble et al. [
1,
2] in 1998. It means that the internal representation of an object within a container must be encapsulated within the object; the mutable part of the internal representation should only be accessed through the object’s interface. If the mutable part of internal representation is directly referenced outside the container and is modified afterward, representation exposure occurs. In the context of Java, the internal representation includes the fields or members of an object defined in a class.
The cause of the occurrence of dynamic or static references to the mutable internal representation outside the container is the use of aliases [
3,
4]. Aliases are produced when different references point to the same object or address at the same time. To better describe how aliases affect encapsulation and therefore lead to representation exposure, we classify aliases into four categories (as shown in
Figure 1) based on the classification introduced by Clarke [
5]. These types are as follows: internal alias A, external alias B, incoming alias C, and outgoing alias D.
Internal aliases (A) refer to an inside object and do not affect anything outside, as depicted in
Figure 1a. For instance, Member b’ refers to Member b in Object a, creating an internal alias.
External aliases (B) are the ones that refer to outside objects, as demonstrated in
Figure 1b. In this case, Object a’ is initialized through external source B.
Incoming aliases (C) are generated when a reference to an external object is passed into the object, as shown in
Figure 1c. Object b is provided for the reference of Member b’ through the interface of Object a, like a setter or constructor. Member b’ is an incoming alias.
Outgoing aliases (D) are created when a reference to an internal object is passed to an external object, as depicted in
Figure 1d. Member b provides a reference to Object b’ through the interface with Object a, such as a getter, resulting in an outgoing alias.
Figure 1.
The four alias types for object passing (the symbol ’ represents an alias object).
Figure 1.
The four alias types for object passing (the symbol ’ represents an alias object).
Internal aliases and external aliases do not involve passing references through interfaces of objects like getters and setters. Therefore, they are generally considered benign [
5]. On the other hand, incoming aliases and outgoing aliases are more likely to cause problems. Incoming aliases expose the internal representation of an object to the outside without the object’s permission [
5], which means the object’s encapsulation is broken. Outgoing aliases offer references to the outside through the object’s interface. Whether representation exposure occurs depends on the way these references are used externally [
5].
Aliases are indispensable in programming languages, although they may lead to the problem of representation exposure. Their reusability plays a crucial part in design patterns like the Observer pattern and the Flyweight pattern, as they involve the use of references to objects [
6,
7]. Furthermore, aliases are also used widely in data structures such as linked lists and hash tables [
8]. Therefore, the goal is to find a way to keep aliases available while preventing the problem of representation exposure from happening.
Issues related to aliasing and representation exposure may also happen in other object-oriented programming languages like C++, Python, Rust, and others. Due to the different characteristics of aliases and encapsulation, each language addresses this problem differently. For instance, Python has a qualifier similar to Java’s
final keyword [
9]; C++ has the
const keyword [
10] and the standard library header “memory”, which provides smart pointers for dynamic memory management [
11]; and Rust introduces features like immutable binding [
12] and reference borrowing [
13] mechanisms. Among the languages and features mentioned above, the C++ smart pointers lean more towards automatic memory management rather than ownership. Unique or shared pointers ensure the expected release of the pointed objects’ memory section, rather than specifying which particular “owner” is able to release or use them. In C++, due to its support for both pass by value and pass by reference functionalities [
14], there are two distinct ways to create aliases. However, in a strict and precise definition, aliases in C++ are mainly generated through passing by reference [
15]. Passing a unique pointer by value will result in a compiler error instead of another copy of it [
16]. However, due to the inability to prevent the creation of aliases through pass by reference [
17], unique pointers are unable to completely address the aliasing issues we aim to discuss in the paper. On the other hand, a shared pointer itself is a type of smart pointer designed to support the creation of aliases [
18]. As for Rust, it can prevent the creation of unexpected aliases, as it has the features of immutability and ownership. Hence, the problem of representation exposure is also prevented.
To help protect encapsulation, mechanisms of immutability [
19] or ownership [
20] are required. Nonetheless, programmers may encounter the exposure issue using Java since it does not provide adequate mechanisms of immutability or ownership [
21]. The keyword “final” in Java only ensures that reassignment to a variable declared as
final after initialization is not allowed. When dealing with mutable objects, there is still a possibility of modifying their internal members [
22]. Programmers can call static factory methods such as
List.of() on the List, Set, and Map interfaces provided by JDK. The methods only ensure the collection itself is not modifiable. If the elements of the collection are mutable, then the contents can still be modified. Furthermore, programmers can only identify the exceptions thrown at runtime if there is any attempt to modify these non-modifiable collections [
23]. Java does not support any built-in mechanism of ownership [
21], which means that it relies on aliasing where references to objects are shared rather than individually owned. Only when assigning a primitive type (e.g.,
int) value to a variable is the value copied, rather than shared through references [
24]. Moreover, Java offers syntaxes for copying objects, such as
new and
clone(), although copying an object is based on shallow copy [
25] if programmers do not implement the deep copy method explicitly. Even though the copied objects appear as distinct entities, their internal members are shared references. This opens the door to potential representation exposure.
So far, some related research has implemented the mechanisms of immutability and ownership to solve representation exposure. At first, Noble et al. proposed an ownership type system [
26] that can be applied to most object-oriented languages. It combines the characteristics of object-oriented languages with ownership types to limit the accessible scope of aliases. The approach protects encapsulation, thus preventing representation exposure from happening. Nevertheless, this approach makes the encapsulation rules too strict to use outgoing aliases due to the lack of object transitivity. In the meantime, if iterators must be used, related iterable classes like
List cannot provide their internal iterators.
Boyapati [
27] introduced a unified type system for improvement with SafeJava. The system achieves ownership by independently specifying owner parameters [
28] and parameterizing classes [
29]. The use of annotations in SafeJava also offers a flexible way to utilize aliasing [
30]. Consequently, programmers can use iterators and outgoing aliases without breaking encapsulation. Since the ownership mechanism of SafeJava requires independent specifications of owner parameters and parameterized classes, it will lead to burdensome parameter annotations [
27,
31,
32]. To achieve a more lightweight method, Potanin et al. integrate the generic mechanism of Confined FeatherWeather Java [
33] with the extended language syntax from Condition Java [
34] to propose Ownership Generic Java (OGJ) [
35]. Y. Zibin, A. Potanin, et al. further presented Ownership Immutability Generic Java (OIGJ) [
21], which dealt with ownership by generic type parameters and solved the iterator problem founded on OGJ and immutability. Nonetheless, representation exposure may still occur because immutability in OIGJ is not transitive [
36]. Furthermore, OIGJ only performs copying for the immutable fields of objects [
21]. Thus, programmers need to define the copying process clearly themselves.
The contributions of the research are as follows:
We survey the limits of protecting encapsulation in the existing related work, focusing on preventing the problem of internal representation exposure in Java.
In the process of researching, we find out there is room for improvement in preventing the issue resulting from mutable alias modification.
This study introduces SlimeJava to achieve ownership in the context of Java, a widely used programming language in industry and academia.
We sequentially present the following content in this paper:
Section 2 explains our research motivation.
Section 3 describes our proposal and implementation; the methods of this paper are presented in the section.
Section 4 evaluates the proposal and describes the results and discussion.
Section 5 discusses the related work.
Section 6 concludes our research.
2. Motivation
Building on the discussion in
Section 1, if we can incorporate the flexible aliasing from SafeJava while maintaining encapsulation, along with features akin to the ones of generic ownership presented in OIGJ, then issues of iterators, representation exposure due to immutability, and shallow copy can be resolved. We aim to employ these strategies to help programmers write programs with less ambiguity and mitigate the problem arising from aliases.
In this section, we illustrate the issue of representation exposure with a concrete example written in Java. Subsequently, we propose an ownership system extended from Java to address the issue. It aids programmers in safeguarding encapsulation to avoid representation exposure by establishing annotations [
31,
37] and source-to-source transformations. We present the details of the methods in
Section 3 and then show the experimental results and discussion in
Section 4 to demonstrate the effectiveness of our proposal. During the testing presented in
Section 4.3, we found that Slime does not cause significant overhead on execution time in comparison to the performance of Java.
To emphasize the importance of representation exposure, it is essential to give examples that are closely related to encapsulation. An instance of a design pattern that utilizes encapsulation is the Memento pattern [
7]. The Memento pattern mainly consists of three roles, which are:
The originator is the object that has an internal state to be saved by the memento object. It can restore the original state from the memento later if needed.
The caretaker stores all the memento objects, allowing the originator to restore its internal state before some operations.
The memento object serves as the core component of the design pattern. It stores the state provided by the originator.
2.1. The Memento Pattern Example
We provide an implementation example using a referenced tweet mechanism [
38]. A programmer would like to employ the Memento pattern to implement a tweet mechanism for storing the state of sent tweets. The state of a tweet includes the tweet’s content, timestamp, and comments. In the main program, the programmer will record the changes in tweet comments for a specific tweet over the development of an hour and then store the tweet state in the caretaker. The implementation process is as follows:
Implement the Tweet class, which includes the tweet’s content (Text), timestamp (Timestamp), and a list of comments (CommentList). Use the private keyword to maintain the encapsulation of all internal fields.
The Originator is responsible for describing objects whose internal state needs to be saved. Here, the tweet is viewed as an object whose internal state requires saving. As a result, we implement the Originator interface with the Tweet class as follows.
The Memento holds the target object state. The Originator can store and restore the object state through the Memento. In our example, the tweet’s Text, Timestamp, and CommentList states are considered the entities to be saved. The tweet state can be stored or restored when necessary.
Lastly, the Caretaker protects the stored Mementos containing the states of target objects. All Mementos related to the tweet are saved in the Caretaker, consequently completing the implementation of the Memento pattern.
However, there is a possibility of encountering representation exposure when using the above implementation in the client-side application. For example, a programmer intends to store tweets that include those from one hour ago along with the current tweet states utilizing the implemented tweet mechanism. The implementation process is as follows:
Declare the necessary variables in advance, such as ONE_HOUR to represent the constant value of one hour, careTaker for safekeeping the mementos, timestamp to record the current time, and commentList to store the list of comments.
Create a new tweet with the content “My first tweet”, a timestamp from one hour ago, and a comment “A”. Then, store the tweet in the Caretaker.
Create another new tweet with the content “My second tweet”, a timestamp from the current time, and a new comment “B”. Store the tweet in the Caretaker.
The programmer expects the saved tweet states (i.e., saveStateList) in the Caretaker to match the content in
Table 1.
Nevertheless, the actual result corresponds to the content in
Table 2.
The first and second tweets’ commentList both have “A” and “B”, which is unexpected. The main program encounters the error when running due to the lack of ownership in Java. The error results from the programmer’s failure to consider the aliasing issue, and it is not noticeable to the programmer at the time of coding. In addition, programmers may unintentionally alter commentLists’ value when calling add() on commentLists due to the absence of immutability in Java.
The Rust programming language with ownership and immutability [
12,
39] can detect this problem and prevent unexpected results. Rust has the feature of immutable binding. It ensures that when performing operations equivalent to “push()” (like “add()” in Java List) on
commentList [
40], these operations cannot be performed if the target object is immutable. Thus, it prevents representation exposure and protects the data of the mementos stored in
saveStateList from unintended external modifications. Finally, programmers can complete the saving of the second
commentList through copying (as shown in the following code listing).
2.2. The Benefits of Designing an Ownership System to Resolve Representation Exposure
The approach in the above main program in Rust involves binding objects that need immutability to prevent modifications. Additionally, Rust incorporates the borrowing mechanism [
13] to allow objects to be borrowed by other non-owning references. Nonetheless, these references cannot access the object outside the scope of their influence. It means that objects must be returned to their owners because they are not owned by the borrowing references. The mechanism realizes the concept of ownership.
By implementing reference borrowing from Rust, we can safely manage aliases and prevent them from unintentionally modifying internal representations. The next benefit is effectively handling the problems resulting from incoming aliases. As ownership systems must address representation exposure caused by incoming aliases, Potanin et al. [
41] discovered that the use of an ownership system had a very small impact on testing performance. Moreover, programmers do not need to spend extra time debugging bugs originating from incoming aliases. We can ensure that encapsulation remains unharmed by unexpected modifications with aliases through the ownership system [
26]. If we can guarantee the encapsulation of objects is not broken, the correctness of program logic and object behavior can be enhanced [
28].
2.3. The Difficulty of Designing an Ownership System to Resolve Representation Exposure
To implement ownership mechanisms in Java, additional design and implementation efforts are required. Currently, many ownership systems designed to address representation exposure primarily rely on rewriting code structures with patterns [
5,
26,
27,
33]. Nonetheless, practical application in Java still has shortcomings that developers must deal with themselves. As mentioned in the introduction, issues resulting from shallow copying arise because OIGJ does not provide mutable object copying capabilities. Additionally, while OIGJ implements immutability to address iterator initialization problems, issues related to non-transitivity may lead to representation exposure [
36].
As a result, we will use the example of accessing iterator operations in iterable classes and separate them as mutable and immutable situations, as shown below.
Mutable iterable class: If we request a mutable iterable class
MutableList to provide an immutable iterator, denoted as
ImmutableIter, we expect that
ImmutableIter can be used in a read-only way by external code. However, we can indirectly modify the contents of
MutableList through
ImmutableIter in OIGJ.
Immutable iterable class: In the case of an immutable iterable class like
ImmutableList, when we request it to provide an iterator, denoted as
Iter, we expect that
ImmutableList will contain mutable objects in a read-only state. However, under the OIGJ mechanism, even though
Iter itself is read-only, it can allow external modifications to the mutable objects contained in
ImmutableList.
3. Proposal and Implementation
We aim to address representation exposure by introducing an annotation system for Java extension, known as SlimeJava, based on the abstract syntax tree. This system enables source-to-source transformations, facilitating the establishment of ownership for programmers. Our ultimate goal is to avoid representation exposure, eliminating unintended mutable alias modifications. To achieve this, we draw inspiration from SafeJava’s flexible alias specifications [
27], OIGJ’s generic ownership features [
21], and Rust’s borrowing mechanism for references [
13]. Additionally, we utilize abstract syntax trees to generate deep copies, resolving the issue of shallow copying. Thereby, the proposal obeys the ownership mechanisms we have designed.
As a result, we introduce three annotations:
SlimeOwned,
SlimeBorrow, and
SlimeCopy.
SlimeOwned represents the declaration of ownership, indicating that the object is possessed by the respective class. In developing this annotation, we took inspiration from SafeJava’s ownership domains [
30] and complied with the typing rules in OIGJ. Additionally, we optimized it for the management of incoming aliases to grant control to the programmer. Next, we took Rust’s borrowing mechanism as a prototype and proposed the
SlimeBorrow annotation, which constrains aliases to the scope of their use and forces their return to the owner once their usage is completed. Lastly, we offer the
SlimeCopy annotation to address the problem of representation exposure arising from shallow copying. By employing abstract syntax trees to generate deep copies, we duplicate objects not owned by any class. This approach relieves programmers from the burden of repeatedly implementing deep copying.
We dive into the details of the annotations used and the contexts in which they are applied in
Section 3.1,
Section 3.2 and
Section 3.3. Using some code examples, we present the functionality of the annotations. Following this, we provide a concise overview of the features of SlimeJava’s proposal. To validate the feasibility and usability of our proposal, we explore the implementation details in
Section 3.5. This section explains how we use abstract syntax trees and the tools employed in this implementation. Finally, in
Section 3.6, we transform the motivating example into another one featuring SlimeJava functionality and analyze how they work.
3.1. SlimeOwned
SlimeOwned signifies the ownership of a Java object In SlimeJava. It indicates that the class possesses the object and that, simultaneously, no external references are made to any values in that object. SlimeOwned applies only to all internal members within the class, including public, protected, and private members, and to method return types (excluding void). For example, in the Java class Owner, we declare ownership of its internal member mObj and set up the setter and getter methods to illustrate the working process.
Ownership Declaration: As the
Owner class owns its internal member
mObj, we can declare
SlimeOwned on
mObj, showing that the
Owner class holds ownership of
mObj.
Setter: When we need to assign
newObj to
mObj, it is represented as follows:
Since
mObj has been declared as
SlimeOwned, indicating a deep copy transformation is required to eliminate the potential of representation exposure resulting from external references, any assignment to
mObj will be converted into a deep copy operation automatically to establish ownership. The code transformed by SlimeJava will be as follows:
Getter: When we need to provide
mObj for external use, we must mark the data as
SlimeOwned for identification.
On the other hand, if an internal member related to
SlimeOwned is not annotated and is returned, then a transformation error will occur before the process of source-to-source transformation begins.
When external code interacts with objects related to
SlimeOwned, our research anticipates that direct access through the owning class itself is the intended approach. For example, when using
mObj in the Owner class, we should only access it by calling
Owner.getObj(). However, due to Java’s inherent aliasing features, disabling aliases would disable functionalities like iterators and parameter passing. Therefore, we regulate aliases to allow mutable aliases according to our expectations. In SlimeJava, we offer two alias management methods for
SlimeOwned objects, which are detailed in
Section 3.2 and
Section 3.3. We explain how to employ these methods through examples. Assume the goal is to use a
List<String> object belonging to the
Owner class in a for loop to print the result "Hello World". We demonstrate correct and incorrect borrowing in sequence.
3.2. SlimeBorrow
The
SlimeBorrow annotation for temporary borrowing of ownership objects is inspired by Rust’s borrowing mechanism [
13]. Due to the lack of immutability in Java, we apply the mutable borrowing mechanism to ensure alias control.
SlimeBorrow can be applied to the passing of parameters, modifications, and deep copying within the context of other
SlimeBorrow annotations. However,
SlimeBorrow cannot provide references, assignments, or returns to arbitrary objects since ownership does not belong to the borrower, and such a return would result in dangling references [
13,
42]. Therefore, we restrict
SlimeOwned objects to be borrowed only once within the same scope, and
SlimeBorrow annotations must be used in the order of borrowing.
Borrowing from
SlimeOwned: When using a
foreach loop with
Owner.getList(), we need to borrow the
List<String> from Owner to operate its values. Therefore, we use SlimeBorrow to obtain its values as str.
Borrowing from SlimeBorrow: You can borrow
str within the
for loop.
Sequential Usage of Borrowing: If you attempt to use the str that has already been borrowed in borrowAgainStr, an error will occur.
Inability to Provide Unexpected Aliases: If you attempt to assign to a non-SlimeBorrow alias, then an assignment error will occur.
3.3. SlimeCopy
The annotation for deep copying of ownership objects, SlimeCopy, generates an entirely new object that does not have the property of SlimeOwned or SlimeBorrow. It is treated as a new Java object. SlimeCopy can only be applied to objects that are annotated as SlimeOwned or SlimeBorrow to represent the copying behavior.
Copying the Entire
List: When we want to print all the strings in a
List using the already written
printListAllString method, we may require the use of copying.
At this point, we can use SlimeCopy on Owner.getList() to perform a deep copy.
Cannot Be Used on Ordinary Objects: When you attempt to use
SlimeCopy on ordinary objects that are not annotated as
SlimeOwned or
SlimeBorrow, an error will occur.
3.4. Summary of the Proposal
In summary, we can achieve the following functionalities through SlimeJava:
Design an ownership system that offers SlimeOwned, SlimeBorrow, and SlimeCopy annotations to help programmers address representation exposure and write programs with less ambiguity.
Avoid unintended modifications of the Tweet state values stored in the caretaker’s Memento through CommentList or Timestamp in the motivating example, thus protecting the encapsulation in this case.
Implement flexible aliasing, as referenced in SafeJava [
27]. It allows the use of aliases without breaking encapsulation and enables iterators to be used with iterable classes. We also take inspiration from Rust’s borrowing mechanism [
13].
In contrast to OIGJ [
21], which cannot handle the copying of mutable internal members, we provide a method for deep copying to prevent the creation of unexpected aliases.
3.5. Implementation Detail
SlimeJava is a source-to-source translator (
https://github.com/ncu-psl/slime-java, accessed on 10 January 2024) developed using the Kotlin [
43] programming language in the environment of Windows 10 64-bit with 32 GB RAM and Java 17. Developed in IntelliJ IDEA [
44], we use the Gradle [
45] build automation tool to create a typical Kotlin development project. Kotlin offers greater flexibility compared to Java, as it comes with features like null safety, smart cast, and co-routines [
46]. Furthermore, Kotlin is compatible with Java. Meanwhile, we implemented it using the annotation system already present in Java to ensure that developers can compile and run code even when SlimeJava is removed. This approach reduces developers’ dependency on a single implementation.
We introduce the stages that SlimeJava goes through. First, we parse Java code using JavaParser. Then, we create a symbol table to check if the syntax adheres to our expected standards. Finally, we hand it over to the Java compiler for further processing after generating the deep copy methods. Due to the nature of generating deep copies, there are some unsupported cases in Java, which we will discuss after all of the SlimeJava stages.
We use the JavaParser [
47] library to parse the code. Listing 1 shows a snippet of code from the Tweet class with SlimeOwned added. We pass the code to JavaParser to generate an abstract syntax tree, whose structure is represented in a YAML [
48] file as shown in
Figure 2. We traverse all the SlimeOwned, SlimeBorrow, and SlimeCopy annotations in our classes. To effectively utilize the parsed code, we build a symbol table during the code traversal process. We use the
VoidVisitorAdapter provided by JavaParser to facilitate code traversal.
Listing 1: A code snippet of Tweet class with SlimeOwned added. |
|
Next, we build the symbol table and utilize it to help us check for annotation validity. For the SlimeOwned annotation, it checks whether the annotation is on a field. When assignments occur for incoming aliases and method returns, the subsequent behavior is tracked. If there is any scope in which this method is called, we further check whether any variable with assignment statements generates unexpected aliases to ensure the ownership system works correctly.
For variables marked with SlimeBorrow, we verify if they are parameters. Additionally, it confirms whether they correspond to other targets marked as SlimeOwned or SlimeBorrow and judges whether subsequent SlimeBorrow annotations use the borrowed reference in the same scope. Regarding variables with SlimeCopy annotations, we need to ensure they are used on the right-hand side of assignments [
49]. Additionally, they should correspond to variables annotated with SlimeOwned and SlimeCopy.
Figure 2.
The AST structure of the code snippet of the Tweet class in the YAML file format.
Figure 2.
The AST structure of the code snippet of the Tweet class in the YAML file format.
The process of checking valid annotations is followed by the stage of transformation. The transformation process involves performing deep copies based on the functionality of SlimeCopy. For internal members marked with SlimeOwned, if the member is not a primitive type, the system will traverse the member’s internal components and automatically generate a deepCopy() method within the member’s class. Subsequently, in the original variables annotated with SlimeCopy, the deepCopy() method will also be automatically applied to complete the transformation process. The process of SlimeJava translation is represented in pseudo-code Algorithm 1.
Algorithm 1 Procedure of SlimeJava Translation |
- 1:
procedure
SlimeJavaTranslate(sourceCode)
▹ using com.github.javaparser [ 47]
- 2:
- 3:
- 4:
- 5:
procedure
checkAnnotationsValidity(symbolTable) - 6:
- 7:
for all nodes in AST do - 8:
if the annotation is not a slime annotation then - 9:
continue - 10:
if the annotation has no parent node then - 11:
continue - 12:
if the annotation is not a marker annotation then - 13:
continue - 14:
if the annotation is not a valid declaration then - 15:
continue - 16:
if the annotation is duplicated then - 17:
continue - 18:
- 19:
procedure
Translate(symbolTable) - 20:
for all members marked with @SlimeOwned do - 21:
if the member is not of primitive type then - 22:
- 23:
for all variables marked with @SlimeCopy do - 24:
|
Because our deep copy generation requires interaction with Java libraries, it encounters resource allocation errors when used with classes that conform to the Singleton pattern, meaning only one instance is permitted in the JVM [
50]. For example, consider the following classes:
GUI (Graphical User Interface) libraries like AWT or Swing [
51].
Thread-related classes, including ThreadPoolExecutor libraries [
52].
Database SQL (Structured Query Language) library drivers [
53].
3.6. Rewriting the Motivating Example for Analysis
We use the example of the Memento design pattern mentioned in
Section 2.1 to demonstrate how a programmer can design a tweet mechanism using SlimeJava. We explain the process step by step, including creating SlimeOwned internal members, preventing issues related to incoming aliases, creating a memento using SlimeCopy, and storing it in a caretaker. Finally, we explain what the client-side instructions are like, what the result is, and any differences from the original example in
Section 2.1.
Creating SlimeOwned internal members: Programmers can explicitly use SlimeOwned provided by SlimeJava to annotate the internal members they want to protect against issues related to representation exposure in the Tweet class. By adding SlimeOwned to
mCommentList, the result is as follows:
Preventing representation exposure resulting from incoming aliases: In the case of the client-side main program, if the client similarly uses the commentLists of tweets from an hour ago and the present, they will encounter the issue of reusing commentList. Since commentList will be passed as an incoming alias to the Tweet class, mCommentList, which has been declared as SlimeOwned, will be automatically transformed into a deep copy through the source-to-source transformation by SlimeJava instead of direct assignment to mCommentList when obtaining an external incoming alias (commentList). The process avoids issues related to the representation exposure originating from incoming aliases.
Establishing the memento with
SlimeCopy and storing it in the Caretaker: Next, the client uses the
careTaker to manage the currently stored
Tweet states. By calling
Tweet.saveToMemento(), a memento of the current state is created. At this point, we encounter the need to store
mText,
mTimestamp, and
mCommentList in a
Memento class that is not SlimeOwned. With the aid of SlimeJava, we can use SlimeCopy to create a complete copy of the data and establish a new
Memento object.
SlimeJava will automatically transform the portion of the code that uses SlimeCopy into a deep copy operation, resulting in the following code:
Now, the
Memento contains new data for
mCommentListState and immutable, primitive data types
mTextState and
mTimestamp. There will not be any issues related to exposing references. We can store the current state of the
tweet in the
caretaker by using the newly created memento.
- 4.
Changes in the subsequent instructions: Now, developers need to change the timestamp to the current time and add a new comment “B” to the
commentList so they can call add() to add “B” to the
commentList. Nevertheless, the call of add() may not change the original mCommentList that was saved when the previous tweet was created. SlimeJava has already handled the issue of exposing representation through incoming aliases. Therefore, developers can pass commentList to the current tweet and save its state in the caretaker.
In the end, the Caretaker is responsible for keeping the data inside the savedStateList. The data in this list will correspond to our expectations (as shown in
Table 1).
4. Evaluation
When considering the relationship between encapsulation and representation exposure issues, key aspects include cloning [
1], iterators [
54], and object encapsulation [
5]. To ensure a fair comparison with other relevant research, we use representative operations in design patterns to evaluate this research [
7]:
Cloning in the Prototype pattern;
Iterator-related operations in the Iterator Pattern;
Object encapsulation in the Memento pattern.
We assess the practicality of SlimeJava in terms of its ability to block issues related to representation exposure. Following this, we analyze the SlimeJava implementation by examining how it assists programmers in preventing problems associated with representation exposure in the context of the motivating example. To demonstrate that our approach both promotes clear coding intention and does not cause an unacceptable execution burden when running the generated code, we employ quantitative evaluations to compare the performance differences between using SlimeJava and not using SlimeJava.
4.1. Design Pattern Operations for Evaluation
We have gathered the common operations of the three design patterns in
Table 3. Based on the four alias types proposed by Clarke [
5], as mentioned in the introduction, we categorize the situations leading to the potential creation of mutable aliases into four types (
Figure 1). We evaluate whether the ownership system can block unexpected mutable aliases, which is critical in assessing the occurrence of representation exposure. These situations include:
Internal Aliases (IL) (
Figure 1a): While they may not directly result in representation exposure in the ownership system, they could cause indirect exposure problems when combined with other aliasing mechanisms.
External Aliases (EL) (
Figure 1b): These aliases are created when external objects directly initialize an object as an alias. Representation exposure is blocked based on whether the operations on this object align with the expected mutable aliasing of the ownership system.
Incoming Aliases (IG) (
Figure 1c): This type of aliasing generates unexpected mutable aliases so it leads to representation exposure in the ownership system. This occurs when two or more objects hold the same internal member through aliases.
Outgoing Aliases (OG) (
Figure 1d): While direct exposure problems may not occur, initializing external objects as aliases through internal members can indirectly cause unexpected mutable aliases outside. This can result in potential representation exposure.
Table 3.
Representative operations in design patterns.
Table 3.
Representative operations in design patterns.
Design Pattern Name | Operations | Explanation | Example |
---|
Prototype | P_INIT | Initialize obj into a Prototype object | new Prototype(obj) |
P_CLONE | Clone a new object based on prototype | prototype.clone() |
Iterator | I_INIT | Initialize a new Container through collection | new Container(collection) |
I_CREATE | Create a new Iterator that belongs to Collection through container | container.createIterator() |
I_FIRST | Show the first object in iterator | iterator.first() |
I_CURRENT | Show the object pointed to by iterator | iterator.current() |
I_NEXT | Move iterator to the next object | iterator.next() |
I_DONE | Show if iterator has reached the last object | iterator.isDone() |
Memento | M_INIT | Initialize a new CareTaker | new CareTaker(savedStates) |
Initialize a new Originator | new Originator(args) |
M_SAVE | Save originator’s current state in Memento | originator.saveToMemento() |
M_RESTORE | Restore originator’s state from memento | originator.restoreFromMemento(memento) |
In
Table 3, “Prototype” represents the cloning operation process, “Iterator” represents the iteration process, and “Memento” represents the object encapsulation process. Each of these design patterns, Prototype, Iterator, and Memento, involves an initialization (INIT) operation. Following that, we provide explanations of the representative operations for each design pattern, with examples to illustrate how these operations are performed.
We have further broken down the content of
Table 3 and organized it into
Table 4 to evaluate the functionality of SlimeJava. In
Table 4, the variables starting with “m” are all internal members.
In
Table 4, we can observe that using SlimeOwned allows developers to achieve expected control over most internal aliases (IL), external aliases (EL), and outgoing aliases (OG). For cloning functionality (CLONE), SlimeCopy is provided to fulfill it. As for incoming aliases (IG) related functionality, it is implemented using SlimeBorrow. However, in the I_NEXT section of Iterator, the ownership system does not apply to the null type (the ownership system does not support objects of null type, which would lead to dangling references [
13,
42]), so it only supports objects of non-null type.
4.2. Comparison with Existing Research Approaches
We now compare the functionality provided by SlimeJava from the operations presented in
Table 3 and
Table 4 with that provided by other proposals. We also include a comparison between the operations of Rust, a language with immutable binding and reference borrowing, and SlimeJava. Additionally, we evaluate how each approach helps prevent representation exposure. This comparison is summarized in
Table 5.
When observing
Table 5 from left to right, we can make the following observations. Due to the lack of immutability and ownership in Java (Version 17), it cannot effectively prevent representation exposure in the above operations. Noble et al.’s ownership types only support iterator operations without incoming and outgoing aliases because of the strict encapsulation. It does not work for I_CREATE since it lacks support for incoming and outgoing aliases. OIGJ proposed by Potanin et al. offers immutability, but it lacks transitivity. This limitation leads to representation exposure during iterator operations. SafeJava, introduced by Boyapati, is more flexible concerning aliasing, which allows it to support a wider range of design pattern operations compared to ownership types and OIGJ. However, it still cannot address the cloning issue encountered in the Prototype operation (P_CLONE).
In the context of Rust, we can observe the comparison of P_CLONE and I_NEXT operations. P_CLONE operation still faces mutable object issues in Rust and cannot prevent representation exposure. I_NEXT operation is fully supported in Rust because it does not have null types like Java. Instead, Rust uses the
Option type with
None to represent the absence of a value [
55]. In contrast, Java is a language that supports null values, even though Java does provide the
Optional library [
56]. Java developers from Oracle announced the library is primarily designed to improve API clarity, not intended for variable declarations or internal members [
57]. Nevertheless, there is no syntax limitation in Java for using Optional. We discuss this topic more in
Section 4.4.
Furthermore, in the case of the P_CLONE operation, our implementation of the deep copy mechanism is better than other proposals. It can effectively address unexpected mutable alias issues.
4.3. Quantitative Performance Evaluation
To prove that our SlimeJava provides ownership features and clear usage intention and prevents representation exposure without affecting the performance of the original program, we conduct runtime timing comparisons by comparing the runtime timing between code with the added SlimeJava annotations to the code without any SlimeJava annotations. We introduce the test code we use and explain how the testing is conducted as well as the objectives of the tests. Subsequently, we present the test results graphically to compare the differences between SlimeJava and standard Java.
We have used the code for the tweet mechanism mentioned in
Section 2.1 and
Section 3.6, incorporating SlimeOwned, SlimeBorrow, and SlimeCopy. We have also rewritten the
Tweet class and main program as follows:
In this code, we used the commentList as a passed parameter and utilized SlimeCopy to assign commentListCopy to the internal member mCommentList. The key difference from the code mentioned earlier is that we generated 100 tweets using a loop to test if our annotations would have any impact on the performance. On the other hand, for the code that does not use SlimeJava, we simply removed the annotations, as SlimeJava is a detachable proposal.
Then, we calculate the time spent in milliseconds to create and save the state to the Memento using Java’s built-in currentTimeMillis method.
After running 100,000 tests (as shown in
Figure 3), we found that Java’s execution time ranged from approximately 9 to 17 milliseconds. In comparison, SlimeJava showed execution time ranging from about 13 to 21 milliseconds because of the added annotation system. In the entire testing, there was no significant difference in execution time such as thousands of milliseconds between the two. The additional overhead was not substantial.
4.4. Discussion
Compared with existing research based on Java in
Table 5, SlimeJava can better prevent representation exposure in the operations specified in
Table 3. As for Rust, it is effective in protecting encapsulation, but it cannot avoid unexpected modifications when it comes to the P_CLONE operation.
Regarding the issue of the
Optional library, as mentioned in
Section 4.2, there is no limitation on syntax in Java. Java programmers need to consider the following scenarios:
Issues in syntax: The objects declared as Optional can still be assigned with null.
Rewriting code: To integrate a no-value mechanism to fulfill the ownership system, programmers need to add “Optional” in every place where null type values might occur, including every parameter declaration, variable declaration, and return value.
Functionalities overlapping with specific classes: In Java, there are classes that can implement the concept of no value, for instance, declaring an empty List using Collections.emptyList(). Adding Optional in such cases might be redundant.
Due to the above considerations, the current use of Java’s Optional might not be the best choice. Apparently, SlimeJava has the same limitations as Java. This is the reason why it cannot fully support the I_NEXT operation (as shown in
Table 4). As we discussed in
Section 4.2 regarding the null type issue, there are currently various research proposals for null safety checkers [
58,
59]. By adopting Java’s built-in Optional library, one can prevent potential null issues and establish an ownership system. Nevertheless, the original intention of Java
Optional [
56] was to deal with specific use cases in functional programming in Java, rather than focusing on an ownership system in object-oriented programming. Hence, it may not be a suitable choice for an ownership system. The Checker Framework [
60,
61] provides the Nullness checker that can check the programs at compile time. If the checker does not issue any warning, then the checked programs will never throw a null pointer exception when running. Assigning a
@Nullable object to a
@NonNull object will result in a warning that the
@NonNull object may become null. Thus, integration with the Nullness checker into SlimeJava is a potential way forward for future work.
At the end of the section, we discuss other future work of the paper. The ownership system of SlimeJava proposed in this paper is implemented through annotations. Similar to what Abi-Antoun et al. [
62] mentioned regarding the visual static ownership domain annotations, we can subsequently integrate it into a Linter [
63], a static analysis tool provided by the development environment, to narrow the gap between programmers’ expectations and the actual results. This enables programmers to visually reduce potential spelling or syntax errors. Through the approach presented in this paper, even though we can utilize Linter, it still relies on programmers to manually avoid unexpected behavior by adding some annotations in their code. If we could automatically analyze potential exposure points, it would significantly assist programmers in identifying and resolving problems more quickly with clearer coding intention. The aforementioned Checker Framework may help us on this subject, as it can check Java programs at compile time to detect errors. For instance, we might define some must-call obligations related to ownership by adding annotations of
@MustCall [
64] and
@EnsuresCalledMethods [
65] on an object in order to detect behaviors that would violate our ownership system. Then, programmers can fix them by applying the approach proposed in the paper.
Another subject we can work on in the future is to survey the feasibility of supporting multiple or/and shared ownership, as well as the features associated with inheritance and classes in SlimeJava. Among the features, interfaces and enums may not need the alias management of SlimeJava since interfaces have no internal members [
66] and all members in enums are unchangeable constants [
67]. As for abstract classes and inner classes, we think it is possible to support them with SlimeJava. However, implementing these features requires extra effort in the source-to-source translation process and more research on issues related to inheritance, which we have not explored. In this paper, we also employed quantitative performance evaluation for the proposed approach with the code mentioned in
Section 2.1,
Section 3.6 and
Section 4.3. To better evaluate the overhead created by SlimeJava’s translation process in terms of scalability, future research could gather some widely adopted open-source projects and run them with SlimeJava.
6. Conclusions
In this research, we addressed the shortcomings in Java related to ownership and immutability, which leads to representation exposure when using encapsulation. We explained that the main problem arises from unexpected mutable aliases, causing internal members to be modified from the outside. Subsequently, we discussed and analyzed existing studies related to representation exposure and proposed a viable method for improvement. We have developed an ownership system called SlimeJava based on abstract syntax trees and source-to-source transformation. This system ensures encapsulation, preventing representation exposure. It includes annotations such as SlimeOwned for assigning ownership to objects and SlimeBorrow and SlimeCopy to handle mutable aliases in a way that is predictable and does not compromise functionality. This maintains code flexibility while effectively tackling representation exposure.
In the implementation, we have verified the feasibility and usability of our proposal by implementing SlimeJava. Our process begins by checking the correctness of the syntax for SlimeOwned, SlimeBorrow, and SlimeCopy. We then perform a source-to-source transformation to generate a deep copying function if needed. For the tools, we adopt the Kotlin language to parse abstract syntax trees using JavaParser, which helps us build our symbol table. Subsequently, we use JavaParser’s VoidVisitorAdapter to traverse the required nodes and add found names to the symbol table. Finally, we use the symbol table to determine the correctness based on the conditions outlined in the proposal. In our evaluation, we choose some commonly used operations from the design patterns associated with copying, iterator-related operations, and encapsulation. We then assess whether our proposal provides sufficient functionality. Additionally, we compare SlimeJava to Rust and other existing research to demonstrate that SlimeJava offers advantages in terms of functionality. We show that our research can better prevent representation exposure, except for a certain operation associated with some limitations in native Java; however, it does not cause an unacceptable burden in execution time.