Java 8 default methods, which allow interfaces to contain (instance) method implementations, are useful for the skeletal implementation software design pattern. However, it is not easy to transform existing software to exploit default methods. In this talk, I discuss an efficient, fully-automated, type constraint-based refactoring approach that assists developers in taking advantage of enhanced interfaces for their legacy Java software.
Unveiling the Tech Salsa of LAMs with Janus in Real-Time Applications
Automated Refactoring of Legacy Java Software to Default Methods Talk at GMU
1. automated refactoring of legacy java
software to default methods
George Mason University
Raffi Khatchadourian 1
Hidehiko Masuhara 2
April 1, 2017
1
Computer Science, Hunter College & the Graduate Center, City University of New York, USA
2
Mathematical and Computing Science, Tokyo Institute of Technology, Japan
5. some history
∙ Java was invented in the 90’s as an Object-Oriented language.
∙ One of the largest changes to the language was in back in 2005
with Java 5.
∙ Generics.
∙ Enhanced for loops.
∙ Annotations.
∙ Type-safe enumerations.
∙ Concurrency enhancements (AtomicInteger).
4
6. java 8 is massive
∙ 10 years later, Java 8 is packed with new features.
∙ Core changes are to incorporate functional language features.
∙ Java 9 is on the horizon (Project Jigsaw, i.e., “modular Java”, etc.)
∙ Our focus is Java 8 enhanced interfaces that allow default
methods.
5
8. java interfaces
An interface is a list of public method declarations (no bodies)
that concrete implementers (classes) must define.
7
9. the java interface type
∙ Cannot be instantiated (abstract).
∙ Cannot contain instance fields.
∙ Can extend other interfaces.
∙ Can contain public static fields and methods (new in Java 8).
A class can implement an multiple interfaces and an interface can
be implemented by multiple classes.
8
10. example
List contains declarations of methods common to all lists.
interface List<E> { //...
/**
* Removes all of the elements from this list
* (optional operation).
* @throws UnsupportedOperationException if the clear
* operation is not supported by this list.
*/
void clear(); /* ... */}
MyList is a particular kind of list.
class MyList<E> implements List<E> { //...
@Override
public void clear() { // not supported.
throw new UnsupportedOperationException();} /*...*/}
A List can refer to MyList instances in clients.
List<E> list = new MyList<>();
9
13. problem
∙ Interface implementers may share common functionality.
∙ Some interface methods may be optional (like List.clear()).
12
14. skeletal implementations
∙ Provide separate abstract class as a partial interface
implementation [1].
∙ Implementers extend the skeletal implementation instead.
∙ Inherit “default” and/or partial interface method definitions.
∙ Makes implementing the interface easier.
13
15. example
interface List<E> { //...
/**
* Removes all of the elements from this list
* (optional operation).
* @throws UnsupportedOperationException if the clear
* operation is not supported by this list.
*/
void clear(); //...
}
abstract class AbstractList<E> implements List<E> {//...
@Override
public void clear() { // not supported.
throw new UnsupportedOperationException();
} //...
}
class MyList<E> extends AbstractList<E> { //...
// Inherits default implementation of clear().
}
14
16. issues
Several issues with this pattern:
Flexibility Java has single class inheritance -> classes cannot
extend classes other than the skeletal implementation
(can use delegation instead [1]).
Evolvability Interface modifications must be in sync with skeletal
implementations in separate classes.
Modularity ∙ No syntatical path from the interface to its
skeletal implementation.
∙ Developers wishing to implement an interface
may need a global analysis to find corresponding
skeletal implementers.
15
19. default methods
∙ Traditionally, interfaces can’t have method definitions (just
declarations).
∙ Default methods supply default implementations of interface
methods.
∙ By default, implementers will receive this implementation if
they don’t provide their own.
18
24. more complicated
Option A:
class MyList<E> implements List<E>, Queue<E> {
@Override
public void clear() {/* ... */}
}
We resolve it manually by overriding the conflicting method.
Option B:
class MyList<E> implements List<E>, Queue<E> {
@Override
public void clear() {
List.super.clear(); // or Queue.super.clear()
}
}
We call the default implementation of method clear() from either
interface List or Queue instead of implementing our own.
23
26. Can eliminate the need for skeletal implementations like
AbstractCollection?
25
27. Cases:
1. Complete migration of skeletal implementation to interface.
∙ Makes type hierarchy less complicated.
∙ One less type.
∙ Makes interfaces easier to implement.
∙ No global analysis required to find skeletal implementation.
∙ Makes interfaces easier to maintain.
∙ No simultaneous modifications between interface and skeletal
implementation required.
2. Partial migration of skeletal implementation to interface.
∙ Possibly makes interfaces easier to implement.
∙ All implementations may not need to extend the skeletal
implementation.
∙ Possibly makes interfaces easier to maintain.
∙ Possibly less simultaneous modifications between interface and
skeletal implementation required.
26
28. research questions
Can eliminate the need for skeletal implementations like
AbstractCollection?
Can we automate this?
1. What are the criteria for an instance method to be converted to
a default method?
2. What if there is a hierarchy of skeletal implementers, e.g.,
AbstractCollection, AbstractList?
3. Can we guarantee semantics preservation?
4. Which client changes are necessary?
27
30. skeletal implementers
Skeletal implementers are abstract classes that provide skeletal
implementations for interfaces.
Let’s take a look at the Java UML class diagram for
AbstractCollection:
29
31. skeletal implementers
Skeletal implementers are abstract classes that provide skeletal
implementations for interfaces.
Let’s take a look at the Java UML class diagram for
AbstractCollection:
29
32. why is this complicated?
Corresponds to converting and migrating a skeletal (instance)
method implementation to an interface.
From a class To an interface
instance method default method
Can we not simply use a “Pull Up Method” [2] refactoring?
Not exactly. The inherent problem is that, unlike classes, interfaces
may extend multiple interfaces. As such, we now have a kind of
multiple inheritance problem to deal with.
30
33. some definitions
Source Method The concrete class instance method for migration to
a default method.
Target Method The abstract interface instance method that will be
transformed to a default method.
Declaring (Origin) Class The class declaring the source method.
Destination Interface The interface declaring the target method.
31
34. some definitions
Source Method The concrete class instance method for migration to
a default method.
Target Method The abstract interface instance method that will be
transformed to a default method.
Declaring (Origin) Class The class declaring the source method.
Destination Interface The interface declaring the target method.
Why not only consider methods?
It’s important to consider the relationship between the declaring
class and the destination interface (type-level preconditions).
31
35. initial refactoring preconditions
Refactoring Preconditions must hold in order to guarantee
semantics preservation.
For this refactoring, some (perhaps) obvious preconditions are:
∙ Source instance method must not access any instance fields.
∙ Interfaces may not have instance fields.
∙ Can’t have a default method in the target interface with the
same signature.
∙ Can’t modify target method’s visibility (all interface methods are
public).
32
36. transformation tasks
Some (perhaps) obvious transformation tasks:
∙ Must replace the target method in the destination interface (i.e.,
add a body to it).
∙ Must prepend the default keyword to the target method.
∙ Must remove any @Override annotations on the source method
(it’s top-level now).
∙ If declaring class is empty remove it? What about references?
∙ What to do with documentation (javadoc) between source and
target methods?.
33
37. more refactoring preconditions
Some (perhaps) not-so-obvious preconditions.
Suppose we have the following situation:
interface I {
void m();
}
interface J {
void m();
}
abstract class A implements I, J {
@Override
void m() {...}
}
Here, A provides a partial implementation of I.m(), which also
happens to be declared as part of interface J.
34
38. more refactoring preconditions
Now, we convert and migrate A.m() to a default method of I:
interface I {
default void m() {..} //was void m();
}
interface J {
void m(); //stays the same.
}
abstract class A implements I, J {
//now empty, was: void m() {...}
}
∙ We now have a compile-time error in A because A needs to
declare which m() it inherits.
∙ Inheritance hierarchies may be large and complicated.
∙ Other preconditions also exist, such as differences between
type- and method- level annotations (e.g., strictfp). 35
40. type constraints to the rescue
Build upon an existing framework of type constraints [3, 4].
For each program element, type constraints denote the subtyping
relationships that must hold between corresponding expressions for
that portion of the system to be considered well-typed.
A complete program is type-correct if all constraints implied by all
program elements hold.
37
41. overriding
Definition (overriding)
A virtual method M in type C overrides a virtual method M′
in type B
if M and M′
have identical signatures and C is equal to B or C is a
subtype of B.1
In this case, we also say that M′
is overridden by M.
1We ignore access rights as interface methods are always public, whose visibility
cannot be reduced. Throws clauses are a topic for future work.
38
42. rootdefs
Definition (root definitions)
Let M be a method. Define:
RootDefs(M) = {M′
| M overrides M′
, and there exists no
M′′
(M′′
̸= M′
) such that M′
overrides M′′
}
39
43. handled exceptions
Let E.m(. . .) be a method call expression. Define:
Handle(E.m(. . .)) = {Exh | Exh is a checked exception
either thrown or caught at E.m(. . .)}
40
44. functional interfaces
Definition (functional)
An interface I is functional iff I is annotated with
@FunctionalInterface or if there exists a lambda expression
implementing I, in which case I has exactly one abstract method
(effectively functional).
41
45. notation and terminology
[E] the type of expression or declaration element E.
[M] the declared return type of method M.
Decl(M) the type that contains method M.
Param(M, i) the i-th formal parameter of method M.
T′
≤ T T′
is equal to T, or T′
is a subtype of T.
T′
< T T′
is a proper subtype of T, i.e., T′
≤ T ∧ T ≰ T′
.
super(C) the super class of class C.
Class(T) iff type T is a class.
Interface(T) iff type T is an interface.
Default(M) iff method M is a default method.
Public(M) iff method M is a public method.
Abstract(M) iff method M has no body.
42
46. Let α be a constraint variable, which can be a type constant T, [E], i.e.,
the type of an expression or declaration element E, Decl(M), i.e., the
type declaring method M, or Decl(F), the type declaring field F. Then,
a type constraint can be one of:
∙ αi ≜ αj, i.e., αi is defined to be the same as αj,
∙ αi ≤ αj, i.e., αi must be equal to or a subtype of αj,
∙ αi = αj, i.e., αi ≤ αj ∧ αj ≤ αi, and
∙ αi < αj, i.e., αi ≤ αj ∧ αj ≰ αi.
Note
If T′
is a class and T is an interface, T′
< T means that T′
implements
T. If both T′
and T are interfaces, then T′
< T means that T′
extends T.
43
48. inferring type constraints
program construct implied type constraint(s)
for every interface I
I ≰ java.lang.Object ∧
∀M[Decl(M) ≜ java.lang.Object∧
Public(M) =⇒ ∃M′
[Decl(M′
) ≜ I∧
NOverrides(M′
, M)]] (12)
for every functional
interface I
∃M[Decl(M) ≜ I ∧ Abstract(M)
∧ ∀M′
[Decl(M′
) ≜ I ∧ M′
̸= M =⇒ ¬Abstract(M′
)]]
(13)
implicit declaration of
this in method M
[this] ≜ Decl(M) (14)
NOverrides(M′, M) ≡ [Param(M′, i)] = [Param(M, i)] ∧ [M′] ≤ [M]
More details in upcoming ICSE 2017 paper [5].
45
50. implementation
Implemented as an open source plug-in to the Eclipse IDE.
Eclipse ASTs with source symbol bindings were used as an
intermediate representation.
Completely separate from the type constraints generated by the
Eclipse Java Developer Tools (JDT).
47
51. filtered contexts
Separated methods into two categories.
The first is (filtered) methods that definitely cannot be refactored or
be participating in the targeted design pattern. This includes
methods not meeting the following criteria:
1. ones whose source is available, writable, and not generated,
2. non-native, concrete instance methods (excluding
constructors) declared in abstract classes implementing at least
one interface whose source is also available, writable, and not
generated,
3. neither synchronized nor final, and
4. overriding at least one non-default interface method.
The remaining are candidates.
48
53. analysis
∼0.144 secs/KLOC, which is practical even for large applications.
Automatically migrated 19.63%, accounting for 652 methods across 19
projects, of the methods that could possibly be participating in the
targeted skeletal implementation pattern in spite of its conservative
nature.
50
55. pull request study
To assess usability, we submitted pull requests to popular open
source Java projects on GitHub.
Of the issued 19 pull requests, 4 have been successfully merged, 5
are open, and 10 were closed without merging.
Merged requests are to projects and frameworks from organizations
such as Eclipse, AOL, and NHL (National Hockey League), and include
the popular Eclipse (formally Goldman Sachs) Collections framework.
In all, the merged projects total 163 watches, 1071 stars, and 180 forks.
Several other projects, although enthusiastic about the approach,
rejected our pull requests, citing reasons such as that they had not
yet moved to Java 8 or needed to support older Java clients at the
time.
52
57. summary
Presented an efficient, fully-automated, type constraint-based,
semantics-preserving approach, featuring an exhaustive rule set,
that migrates the skeletal implementation pattern in legacy Java
code to instead use default methods.
Implemented as an Eclipse IDE plug-in and was evaluated on 19
open source projects.
Tool scales and was able to refactor, despite its conservativeness
and language constraints, 19.63% of all methods possibly
participating in the pattern with minimal intervention.
Highlights pattern usage and gives insight to language designers on
applicability to existing software.
4 pull requests were merged into GitHub repositories, which include
large, widely used frameworks from reputable organizations.
54
58. summary
Get the source and download the prototype Eclipse plug-in
presentation from:
github.com/khatchad/Java-8-Interface-Refactoring
Visit the project website:
cuny.is/interefact
More about my research and my lab:
cuny.is/khatchad
55
59. for further reading
J. Bloch, Effective Java. Prentice Hall, 2008.
M. Fowler, Refactoring: Improving the Design of Existing Code.
Addison-Wesley Professional, 1999.
J. Palsberg and M. I. Schwartzbach, Object-oriented type systems.
John Wiley and Sons Ltd., 1994.
F. Tip, R. M. Fuhrer, A. Kieżun, M. D. Ernst, I. Balaban, and
B. De Sutter, “Refactoring using type constraints,” ACM
Transactions on Programming Languages and Systems, vol. 33,
no. 3, pp. 9:1–9:47, May 2011.
Raffi Khatchadourian and Hidehiko Masuhara.
Automated refactoring of legacy java software to default
methods.
In International Conference on Software Engineering (to appear),
ICSE ’17. ACM/IEEE, 2017.
56