Automated Refactoring of Legacy Java Software to Default Methods Raffi Khatchadourian1 Hidehiko Masuhara2 1 Hunter College & the Graduate Center, City University of New York 2 Tokyo Institute of Technology Introduction Java 8 introduces enhanced interfaces, allowing for default (instance) methods that implementers will in- herit if none are provided [3]. Default methods can be used [2] as a replacement of the skeletal imple- mentation pattern [1], which creates abstract skeletal implementation classes that implementers extend. Migrating legacy code using the skeletal implementa- tion pattern to instead use default methods can re- quire significant manual effort due to subtle language and semantic restrictions. It requires preserving type- correctness by analyzing complex type hierarchies, re- solving issues arising from multiple inheritance, recon- ciling differences between class and interface methods, and ensuring tie-breakers with overriding class methods do not alter semantics. We propose an efficient, fully-automated, semantics- preserving refactoring approach, based on type con- straints [4,5] and implemented as an open source Eclipse plug-in, that assists developers in taking ad- vantage of enhanced interfaces. It identifies instances of the pattern and safely migrates class method imple- mentations to interfaces as default methods. Motivating Example Consider the following interface: 1 interface Collection<E> { 2 int size(); 3 void setSize(int i) throws Exception; 4 void add(E elem); // optional. 5 void removeLast(); 6 boolean isEmpty(); 7 int capacity();} And the following skeletal implementation classes: 1 abstract class AbstractCollection<E> implements Collection<E>, 2 Object[] elems; int size; // instance fields. 3 @Override public int size() {return this.size;} 4 @Override public void setSize(int i) {this.size = i;} 5 @Override public void add(E elem) 6 {throw new UnsupportedOperationException();}} 7 @Override public void removeLast() 8 {if (!isEmpty()) this.setSize(this.size()-1);} 9 @Override public boolean isEmpty() {return this.size()==0;} 10 @Override public int capacity() {return this.elems.length;}} Implementers now extend AbstractCollection to inherit the skeletal implementations of Collection: 1 // inherits skeletal implementations: 2 List<String> list = new AbstractList<>() {}; And override its methods as needed: 1 List<string> immutableList = new AbstractList<> { 2 @Override public void add(E elem) { 3 throw new UnsupportedOperationException();}} This pattern has several drawbacks, including: Inheritance. Implementers extending AbstractCollection cannot further extend. Modularity. No syntactic path between Collection and AbstractCollection. Bloated libraries. Skeletal implementation classes must be separate from interfaces. Refactoring Approach Can we refactor abstract skeletal implementation classes to instead utilize default methods? Type Constraints [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. Figure 1: Type constraint notation (inspired by [5]). We use type constraints [4,5] to express refactoring preconditions. For each program element, type con- straints denote the subtyping relationships that must hold between corresponding expressions for that por- tion of the system to be considered well-typed. Thus, a complete program is type-correct if all constraints implied by all program elements hold. Fig. 2 depicts several constraints used. program construct implied type constraint(s) method call E.m(E1, . . . , En) to a virtual method M (throwing exceptions Ext1, . . . , Extj) [E.m(E1, . . . , En)] [M] (1) [Ei] ≤ [Param(M, i)] (2) [E] ≤ Decl(M1) ∨ · · · ∨ [E] ≤ Decl(Mk) (3) where RootDefs(M) = {M1, . . . , Mk} ∀Ext ∈ {Ext1, . . . , Extj} (4) ∃Exh ∈ Handle(E.m(E1, . . . , En)) [[Ext] ≤ [Exh]] access E.f to field F [E.f] [F] (5) [E] ≤ Decl(F) (6) Figure 2: Example type constraints. Inferring Type Constraints Type constraints are used to determine if a possible migration will either result in a type-incorrect or se- mantically different program. For example: • Migrating size() and capacity() from AbstractCollection to Collection implies that [this] = Collection, violating constraint (6) that [this] ≤ [AbstractCollection]. • Migrating AbstractCollection.setSize() to Collection implies that the method now throws an Exception, violating constraint (4) as Handle(this.setSize(this.size()-1)) = ∅. • add(), removeLast(), and isEmpty() from AbstractCollection can be safely migrated to Collection as a default method (Lst. 1) since there are no type constraint violations. 1 interface Collection<E> { 2 int size(); 3 void setSize(int i) throws Exception; 4 default void add(E elem) // optional. 5 {throw new UnsupportedOperationException();} 6 default void removeLast() 7 {if (!isEmpty()) this.setSize(this.size()-1);} 8 default boolean isEmpty() {return this.size()==0;} 9 int capacity(); 10 abstract class AbstractCollection<E> implements Collection<E> { 11 Object[] elems; int size; // instance fields. 12 @Override public int size() {return this.size;} 13 @Override public void setSize(int i) {this.size = i;} 14 @Override public void add(E elem) 15 {throw new UnsupportedOperationException();}} 16 @Override public void removeLast() 17 {if (!isEmpty()) this.setSize(this.size()-1);} 18 @Override public boolean isEmpty(){return this.size()==0;} 19 @Override public int capacity() {return this.elems.length;} 1: Refactored version. Evaluation subject KL KM cnds dflts fps δ -δ tm (s) ArtOfIllusion 118 6.94 16 1 34 1 0 3.65 Azureus 599 3.98 747 116 1366 31 2 61.83 Colt 36 3.77 69 4 140 3 0 6.76 elasticsearch 585 47.87 339 69 644 21 4 83.30 Java8 291 30.99 299 93 775 25 10 64.66 JavaPush 6 0.77 1 0 4 0 0 1.02 JGraph 13 1.47 16 2 21 1 0 3.12 JHotDraw 32 3.60 181 46 282 8 0 7.75 JUnit 26 3.58 9 0 25 0 0 0.79 MWDumper 5 0.40 11 0 24 0 0 0.29 osgi 18 1.81 13 3 11 2 0 0.76 rdp4j 2 0.26 10 8 2 1 0 1.10 spring 506 53.51 776 150 1459 50 13 91.68 Tomcat 176 16.15 233 31 399 13 0 13.81 verbose 4 0.55 1 0 1 0 0 0.55 VietPad 11 0.58 15 0 26 0 0 0.36 Violet 27 2.06 104 40 102 5 1 3.54 Wezzle2D 35 2.18 87 13 181 5 0 4.26 ZKoss 185 15.95 394 76 684 0 0 33.95 Totals: 2677 232.2 3321 652 6180 166 30 383.17 Table 1: Experimental results. Able to automatically migrate 19.63% (column dflts) of candidate methods despite of its conservatism. Figure 3: Precondition failures Preliminary Pull Request Study Submitted 19 pull requests to Java projects on GitHub, of which 4 have been successfully merged, 5 are open, and 10 were closed without merging. Merged projects totaled 163 watches, 1071 stars, and 180 forks. Projects rejecting requests citing reasons such as that they needed to support older Java clients. Conclusion We have presented an efficient, fully-automated, type constraint-based, semantics-preserving approach, fea- turing an exhaustive rule set, that migrates the skeletal implementation pattern in legacy Java code to instead use default methods. It is implemented as an Eclipse IDE plug-in and was evaluated on 19 open source projects. The results show that our 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. Our study highlights pattern usage and gives insight to language designers on applicability to existing software. References [1] Joshua Bloch. Effective Java. Prentice Hall, 2008. [2] Brian Goetz. Interface evolution via virtual extensions methods. Tech- nical report, Oracle Corporation, June 2011. [3] Oracle Corporation. Java Programming Language Enhancements. [4] Jens Palsberg and Michael I. Schwartzbach. Object-oriented type sys- tems. John Wiley and Sons Ltd., 1994. [5] Frank Tip, Robert M. Fuhrer, Adam Kieżun, Michael D. Ernst, Ittai Bal- aban, and Bjorn De Sutter. Refactoring using type constraints. ACM TOPLAS, 33(3):9:1–9:47, May 2011. International Conference on Software Engineering, May 20–28, 2017, Buenos Aires, Argentina

Poster on Automated Refactoring of Legacy Java Software to Default Methods

  • 1.
    Automated Refactoring ofLegacy Java Software to Default Methods Raffi Khatchadourian1 Hidehiko Masuhara2 1 Hunter College & the Graduate Center, City University of New York 2 Tokyo Institute of Technology Introduction Java 8 introduces enhanced interfaces, allowing for default (instance) methods that implementers will in- herit if none are provided [3]. Default methods can be used [2] as a replacement of the skeletal imple- mentation pattern [1], which creates abstract skeletal implementation classes that implementers extend. Migrating legacy code using the skeletal implementa- tion pattern to instead use default methods can re- quire significant manual effort due to subtle language and semantic restrictions. It requires preserving type- correctness by analyzing complex type hierarchies, re- solving issues arising from multiple inheritance, recon- ciling differences between class and interface methods, and ensuring tie-breakers with overriding class methods do not alter semantics. We propose an efficient, fully-automated, semantics- preserving refactoring approach, based on type con- straints [4,5] and implemented as an open source Eclipse plug-in, that assists developers in taking ad- vantage of enhanced interfaces. It identifies instances of the pattern and safely migrates class method imple- mentations to interfaces as default methods. Motivating Example Consider the following interface: 1 interface Collection<E> { 2 int size(); 3 void setSize(int i) throws Exception; 4 void add(E elem); // optional. 5 void removeLast(); 6 boolean isEmpty(); 7 int capacity();} And the following skeletal implementation classes: 1 abstract class AbstractCollection<E> implements Collection<E>, 2 Object[] elems; int size; // instance fields. 3 @Override public int size() {return this.size;} 4 @Override public void setSize(int i) {this.size = i;} 5 @Override public void add(E elem) 6 {throw new UnsupportedOperationException();}} 7 @Override public void removeLast() 8 {if (!isEmpty()) this.setSize(this.size()-1);} 9 @Override public boolean isEmpty() {return this.size()==0;} 10 @Override public int capacity() {return this.elems.length;}} Implementers now extend AbstractCollection to inherit the skeletal implementations of Collection: 1 // inherits skeletal implementations: 2 List<String> list = new AbstractList<>() {}; And override its methods as needed: 1 List<string> immutableList = new AbstractList<> { 2 @Override public void add(E elem) { 3 throw new UnsupportedOperationException();}} This pattern has several drawbacks, including: Inheritance. Implementers extending AbstractCollection cannot further extend. Modularity. No syntactic path between Collection and AbstractCollection. Bloated libraries. Skeletal implementation classes must be separate from interfaces. Refactoring Approach Can we refactor abstract skeletal implementation classes to instead utilize default methods? Type Constraints [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. Figure 1: Type constraint notation (inspired by [5]). We use type constraints [4,5] to express refactoring preconditions. For each program element, type con- straints denote the subtyping relationships that must hold between corresponding expressions for that por- tion of the system to be considered well-typed. Thus, a complete program is type-correct if all constraints implied by all program elements hold. Fig. 2 depicts several constraints used. program construct implied type constraint(s) method call E.m(E1, . . . , En) to a virtual method M (throwing exceptions Ext1, . . . , Extj) [E.m(E1, . . . , En)] [M] (1) [Ei] ≤ [Param(M, i)] (2) [E] ≤ Decl(M1) ∨ · · · ∨ [E] ≤ Decl(Mk) (3) where RootDefs(M) = {M1, . . . , Mk} ∀Ext ∈ {Ext1, . . . , Extj} (4) ∃Exh ∈ Handle(E.m(E1, . . . , En)) [[Ext] ≤ [Exh]] access E.f to field F [E.f] [F] (5) [E] ≤ Decl(F) (6) Figure 2: Example type constraints. Inferring Type Constraints Type constraints are used to determine if a possible migration will either result in a type-incorrect or se- mantically different program. For example: • Migrating size() and capacity() from AbstractCollection to Collection implies that [this] = Collection, violating constraint (6) that [this] ≤ [AbstractCollection]. • Migrating AbstractCollection.setSize() to Collection implies that the method now throws an Exception, violating constraint (4) as Handle(this.setSize(this.size()-1)) = ∅. • add(), removeLast(), and isEmpty() from AbstractCollection can be safely migrated to Collection as a default method (Lst. 1) since there are no type constraint violations. 1 interface Collection<E> { 2 int size(); 3 void setSize(int i) throws Exception; 4 default void add(E elem) // optional. 5 {throw new UnsupportedOperationException();} 6 default void removeLast() 7 {if (!isEmpty()) this.setSize(this.size()-1);} 8 default boolean isEmpty() {return this.size()==0;} 9 int capacity(); 10 abstract class AbstractCollection<E> implements Collection<E> { 11 Object[] elems; int size; // instance fields. 12 @Override public int size() {return this.size;} 13 @Override public void setSize(int i) {this.size = i;} 14 @Override public void add(E elem) 15 {throw new UnsupportedOperationException();}} 16 @Override public void removeLast() 17 {if (!isEmpty()) this.setSize(this.size()-1);} 18 @Override public boolean isEmpty(){return this.size()==0;} 19 @Override public int capacity() {return this.elems.length;} 1: Refactored version. Evaluation subject KL KM cnds dflts fps δ -δ tm (s) ArtOfIllusion 118 6.94 16 1 34 1 0 3.65 Azureus 599 3.98 747 116 1366 31 2 61.83 Colt 36 3.77 69 4 140 3 0 6.76 elasticsearch 585 47.87 339 69 644 21 4 83.30 Java8 291 30.99 299 93 775 25 10 64.66 JavaPush 6 0.77 1 0 4 0 0 1.02 JGraph 13 1.47 16 2 21 1 0 3.12 JHotDraw 32 3.60 181 46 282 8 0 7.75 JUnit 26 3.58 9 0 25 0 0 0.79 MWDumper 5 0.40 11 0 24 0 0 0.29 osgi 18 1.81 13 3 11 2 0 0.76 rdp4j 2 0.26 10 8 2 1 0 1.10 spring 506 53.51 776 150 1459 50 13 91.68 Tomcat 176 16.15 233 31 399 13 0 13.81 verbose 4 0.55 1 0 1 0 0 0.55 VietPad 11 0.58 15 0 26 0 0 0.36 Violet 27 2.06 104 40 102 5 1 3.54 Wezzle2D 35 2.18 87 13 181 5 0 4.26 ZKoss 185 15.95 394 76 684 0 0 33.95 Totals: 2677 232.2 3321 652 6180 166 30 383.17 Table 1: Experimental results. Able to automatically migrate 19.63% (column dflts) of candidate methods despite of its conservatism. Figure 3: Precondition failures Preliminary Pull Request Study Submitted 19 pull requests to Java projects on GitHub, of which 4 have been successfully merged, 5 are open, and 10 were closed without merging. Merged projects totaled 163 watches, 1071 stars, and 180 forks. Projects rejecting requests citing reasons such as that they needed to support older Java clients. Conclusion We have presented an efficient, fully-automated, type constraint-based, semantics-preserving approach, fea- turing an exhaustive rule set, that migrates the skeletal implementation pattern in legacy Java code to instead use default methods. It is implemented as an Eclipse IDE plug-in and was evaluated on 19 open source projects. The results show that our 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. Our study highlights pattern usage and gives insight to language designers on applicability to existing software. References [1] Joshua Bloch. Effective Java. Prentice Hall, 2008. [2] Brian Goetz. Interface evolution via virtual extensions methods. Tech- nical report, Oracle Corporation, June 2011. [3] Oracle Corporation. Java Programming Language Enhancements. [4] Jens Palsberg and Michael I. Schwartzbach. Object-oriented type sys- tems. John Wiley and Sons Ltd., 1994. [5] Frank Tip, Robert M. Fuhrer, Adam Kieżun, Michael D. Ernst, Ittai Bal- aban, and Bjorn De Sutter. Refactoring using type constraints. ACM TOPLAS, 33(3):9:1–9:47, May 2011. International Conference on Software Engineering, May 20–28, 2017, Buenos Aires, Argentina