Kotlin: Uncovered

Kotlin: Uncovered

Kotlin does a lot for us in the way of reducing boilerplate. But what is it really doing? We will be inspecting some decompiled Kotlin to discover how it does its job. By looking underneath at how it handles data classes, lambdas, and delegation, we can better understand how the language executes what we write. If you’re curious about the language, or already using it in production, you should walk away from this investigation with a deeper understanding of Kotlin, and some tools for continued exploration.

- As given at Droidcon Boston '17 and Chicago Roboto '17

2edeb6ac352e7d7ec1fc6587def47042?s=128

Victoria Gonda

April 05, 2017
Tweet

Transcript

  1. @TTGonda collectiveidea.com Kotlin: Uncovered Victoria Gonda www.victoriagonda.com @TTGonda

  2. @TTGonda collectiveidea.com Holland, MI Dancer Collective Idea

  3. @TTGonda collectiveidea.com I ❤ Programming Languages

  4. @TTGonda collectiveidea.com DEFINE('BUBBLE(A,ALEN)I,J,UB,TMP') : (BUBBLE_END) BUBBLE I = 1; UB

    = ALEN OUTER GT(UB,1) :F(BDONE) J = 1 INNER LE(A<J>, A<J + 1>) :S(INCRJ) TMP = A<J> A<J> = A<J + 1> A<J + 1> = TMP INCRJ J = LT(J + 1,UB) J + 1 :S(INNER) UB = UB - 1 :(OUTER) BDONE BUBBLE = A :(RETURN) BUBBLE_END NUMBERS = '33 99 15 54 1 20 88 47 68 72' OUTPUT = "Unsorted List" OUTPUT = NUMBERS; NUMARRAY = ARRAY(10) FLOOP I = I + 1 NUMBERS SPAN('0123456789') . NUMARRAY<I> = :S(FLOOP) BUBBLE(NUMARRAY,10); NUMBERS = '' SLOOP J = J + 1; NUMBERS = NUMBERS NUMARRAY<J> ' ' :S(SLOOP) OUTPUT = TRIM(NUMBERS) END SNOBOL
  5. @TTGonda collectiveidea.com HAI 1.2 CAN HAS STDIO? VISIBLE "IS KITTAH

    GAME!" VISIBLE "ENTER 'HELP' FOR CONTROLS" VISIBLE "" I HAS A NAME VISIBLE "WHAT IS KITTAH'S NAME?" GIMMEH NAME I HAS A ACTION VISIBLE SMOOSH "WAT U WANTING TO DO WITH " NAME "?" MKAY GIMMEH ACTION IM IN YR LOOP NERFIN YR PURRS TIL BOTH SAEM ACTION AN "KBYE" BOTH SAEM ACTION AN "HELP", O RLY? YA RLY VISIBLE "PET" VISIBLE "FEED" VISIBLE "IS HAPPY?" VISIBLE "KBYE" MEBBE BOTH SAEM ACTION AN "PURRS" VISIBLE SMOOSH "PURRS: " PURRS MKAY MEBBE BOTH SAEM ACTION AN "FEED" VISIBLE SMOOSH "YOU DID FEED " NAME ". " NAME " IS HAPPY WITH FOOD." MKAY PURRS R SUM OF PURRS AN 3 LOLCODE
  6. @TTGonda collectiveidea.com

  7. @TTGonda collectiveidea.com “Statically typed programming language for the JVM, Android

    and the browser”
  8. @TTGonda collectiveidea.com Statically Typed

  9. @TTGonda collectiveidea.com Statically Typed final String name = "Victoria"; val

    name = "Victoria"
  10. @TTGonda collectiveidea.com Null Safety

  11. @TTGonda collectiveidea.com Null Safety NullPointerException

  12. @TTGonda collectiveidea.com Concise

  13. @TTGonda collectiveidea.com map{$P=$P[$f^ord($p{$_})&6];$p{$_}=/ ^$P/ix?$P:close$_} Perl

  14. @TTGonda collectiveidea.com

  15. @TTGonda collectiveidea.com Java Virtual Machine

  16. @TTGonda collectiveidea.com Java Virtual Machine

  17. @TTGonda collectiveidea.com

  18. @TTGonda collectiveidea.com

  19. @TTGonda collectiveidea.com Data Classes Null Safety Delegation Class Extensions Lambdas

  20. @TTGonda collectiveidea.com Data Classes

  21. @TTGonda collectiveidea.com class User( val firstName: String, var lastName: String?

    )
  22. @TTGonda collectiveidea.com public final class User { @NotNull private final

    String firstName; @Nullable private String lastName; public User(@NotNull String firstName, @Nullable String lastName) { Intrinsics.checkParameterIsNotNull(firstName, "firstName"); super(); this.firstName = firstName; this.lastName = lastName; } @NotNull public final String getFirstName() { return this.firstName; } @Nullable public final String getLastName() { return this.lastName; } public final void setLastName(@Nullable String var1) { this.lastName = var1; } }
  23. @TTGonda collectiveidea.com public final class User {

  24. @TTGonda collectiveidea.com @NotNull private final String firstName; @Nullable private String

    lastName; public final class User {
  25. @TTGonda collectiveidea.com public User(@NotNull String firstName, @Nullable String lastName) {

    Intrinsics.checkParameterIsNotNull(firstName, "firstName"); super(); this.firstName = firstName; this.lastName = lastName; } public final class User { @NotNull private final String firstName; @Nullable private final String lastName;
  26. @TTGonda collectiveidea.com public User(@NotNull String firstName, @Nullable String lastName) {

    Intrinsics.checkParameterIsNotNull(firstName, "firstName"); super(); this.firstName = firstName; this.lastName = lastName; } public final class User { @NotNull private final String firstName; @Nullable private final String lastName; public static void checkParameterIsNotNull( Object value, String paramName) { if (value == null) { // prints error with stack trace throwParameterIsNullException(paramName); } }
  27. @TTGonda collectiveidea.com public User(@NotNull String firstName, @Nullable String lastName) {

    Intrinsics.checkParameterIsNotNull(firstName, "firstName"); super(); this.firstName = firstName; this.lastName = lastName; } public final class User { @NotNull private final String firstName; @Nullable private final String lastName; public static void checkParameterIsNotNull( Object value, String paramName) { if (value == null) { // prints error with stack trace throwParameterIsNullException(paramName); } } Caused by: java.lang.IllegalStateException: firstName must not be null at com.project.User.<init>(User.kt:8)
  28. @TTGonda collectiveidea.com public User(@NotNull String firstName, @Nullable String lastName) {

    Intrinsics.checkParameterIsNotNull(firstName, "firstName"); super(); this.firstName = firstName; this.lastName = lastName; } public final class User { @NotNull private final String firstName; @Nullable private final String lastName;
  29. @TTGonda collectiveidea.com @NotNull public final String getFirstName() { return this.firstName;

    } @Nullable public final String getLastName() { return this.lastName; } public final void setLastName(@Nullable String var1) { this.lastName = var1; } } Intrinsics.checkParameterIsNotNull(lastName, "lastName"); super(); this.firstName = firstName; this.lastName = lastName; }
  30. @TTGonda collectiveidea.com data class User( val firstName: String, var lastName:

    String? )
  31. @TTGonda collectiveidea.com public final class User { @NotNull private final

    String firstName; @Nullable private String lastName; public User(@NotNull String firstName, @Nullable String lastName) { Intrinsics.checkParameterIsNotNull(firstName, "firstName"); super(); this.firstName = firstName; this.lastName = lastName; } @NotNull public final String getFirstName() { return this.firstName; } @Nullable public final String getLastName() { return this.lastName; } public final void setLastName(@Nullable String var1) { this.lastName = var1; } @NotNull public final String component1() { return this.firstName; } @Nullable public final String component2() { return this.lastName; } @NotNull public final User copy(@NotNull String firstName, @Nullable String lastName) { Intrinsics.checkParameterIsNotNull(firstName, "firstName"); return new User(firstName, lastName); } // $FF: synthetic method // $FF: bridge method @NotNull public static User copy$default(User var0, String var1, String var2, int var3, Object var4) { if((var3 & 1) != 0) { var1 = var0.firstName; } if((var3 & 2) != 0) { var2 = var0.lastName; } return var0.copy(var1, var2); } public String toString() { return "User(firstName=" + this.firstName + ", lastName=" + this.lastName + ")"; } public int hashCode() { return (this.firstName != null?this.firstName.hashCode():0) * 31 + (this.lastName != null?this.lastName.hashCode():0); } public boolean equals(Object var1) { if(this != var1) { if(var1 instanceof User) { User var2 = (User)var1; if(Intrinsics.areEqual(this.firstName, var2.firstName) && Intrinsics.areEqual(this.lastName, var2.lastName)) { return true; } } return false; } else { return true; } } }
  32. @TTGonda collectiveidea.com @NotNull public final String component1() { return this.firstName;

    } @Nullable public final String component2() { return this.lastName; } } public final void setLastName(@Nullable String this.lastName = var1; }
  33. @TTGonda collectiveidea.com @NotNull public final String component1() { return this.firstName;

    } @Nullable public final String component2() { return this.lastName; } } public final void setLastName(@Nullable String this.lastName = var1; } // Destructuring Data Class Declarations val user = User("Victoria", "Gonda") val (firstname, lastname) = user
  34. @TTGonda collectiveidea.com @NotNull public final String component1() { return this.firstName;

    } @Nullable public final String component2() { return this.lastName; } } public final void setLastName(@Nullable String this.lastName = var1; }
  35. @TTGonda collectiveidea.com @NotNull public final User copy( @NotNull String firstName,

    @Nullable String lastName) { Intrinsics .checkParameterIsNotNull(firstName, "firstName"); return new User(firstName, lastName); } return this.firstName; } @Nullable public final String component2() { return this.lastName; }
  36. @TTGonda collectiveidea.com // $FF: synthetic method // $FF: bridge method

    @NotNull public static User copy$default( User var0, String var1, String var2, int var3, Object var4) { if((var3 & 1) != 0) { var1 = var0.firstName; } if((var3 & 2) != 0) { var2 = var0.lastName; } return var0.copy(var1, var2); } public final User copy( @NotNull String firstName, @Nullable String lastName) { Intrinsics .checkParameterIsNotNull(firstName, "firstName"); return new User(firstName, lastName); }
  37. @TTGonda collectiveidea.com public String toString() { return "User(firstName=" + this.firstName

    + ", lastName=" + this.lastName + ")"; } if((var3 & 1) != 0) { var1 = var0.firstName; } if((var3 & 2) != 0) { var2 = var0.lastName; } return var0.copy(var1, var2); }
  38. @TTGonda collectiveidea.com public int hashCode() { return (this.firstName != null?

    this.firstName.hashCode():0) * 31 + (this.lastName != null? this.lastName.hashCode():0); } public String toString() { return "User(firstName=" + this.firstName + ", lastName=" + this.lastName + ")"; }
  39. @TTGonda collectiveidea.com public boolean equals(Object var1) { if(this != var1)

    { if(var1 instanceof User) { User var2 = (User)var1; if(Intrinsics.areEqual(this.firstName, var2.firstName) && Intrinsics.areEqual(this.lastName, var2.lastName)) { return true; } } return false; } else { return true; } } } this.firstName.hashCode():0) * 31 + (this.lastName != null? this.lastName.hashCode():0); }
  40. @TTGonda collectiveidea.com public final class User { @NotNull private final

    String firstName; @Nullable private String lastName; public User(@NotNull String firstName, @Nullable String lastName) { Intrinsics.checkParameterIsNotNull(firstName, "firstName"); super(); this.firstName = firstName; this.lastName = lastName; } @NotNull public final String getFirstName() { return this.firstName; } @Nullable public final String getLastName() { return this.lastName; } public final void setLastName(@Nullable String <set-?>) { this.lastName = <set-?>; } @NotNull public final String component1() { return this.firstName; } @Nullable public final String component2() { return this.lastName; } @NotNull public final User copy(@NotNull String firstName, @Nullable String lastName) { Intrinsics.checkParameterIsNotNull(firstName, "firstName"); return new User(firstName, lastName); } // $FF: synthetic method // $FF: bridge method @NotNull public static User copy$default(User var0, String var1, String var2, int var3, Object var4) { if((var3 & 1) != 0) { var1 = var0.firstName; } if((var3 & 2) != 0) { var2 = var0.lastName; } return var0.copy(var1, var2); } public String toString() { return "User(firstName=" + this.firstName + ", lastName=" + this.lastName + ")"; } public int hashCode() { return (this.firstName != null?this.firstName.hashCode():0) * 31 + (this.lastName != null?this.lastName.hashCode():0); } public boolean equals(Object var1) { if(this != var1) { if(var1 instanceof User) { User var2 = (User)var1; if(Intrinsics.areEqual(this.firstName, var2.firstName) && Intrinsics.areEqual(this.lastName, var2.lastName)) { return true; } } return false; } else { return true; } } }
  41. @TTGonda collectiveidea.com data class User( val firstName: String = "Victoria",

    var lastName: String? ) Default Values
  42. @TTGonda collectiveidea.com val user = User(
 lastName = "Gonda" )

    Named Parameters
  43. @TTGonda collectiveidea.com Null Safety

  44. @TTGonda collectiveidea.com // Wont compile
 var maybeString: String? = "Hello"


    maybeString.length
  45. @TTGonda collectiveidea.com Safe Call Operator val maybeString: String? = "Hello"

    maybeString?.length
  46. @TTGonda collectiveidea.com String maybeString = "Hello"; maybeString.length(); ? ? ?

  47. @TTGonda collectiveidea.com Safe Call Operator val maybeString: String? = null

    maybeString?.length
  48. @TTGonda collectiveidea.com String maybeString = (String) null; if(maybeString != null)

    { maybeString.length(); }
  49. @TTGonda collectiveidea.com val maybeString: String? = null maybeString!!.length // NPE!

  50. @TTGonda collectiveidea.com String maybeString = (String) null; if(maybeString == null)

    { Intrinsics.throwNpe(); } maybeString.length();
  51. @TTGonda collectiveidea.com val maybeString: String? = null maybeString?.let { string

    -> string.length } let
  52. @TTGonda collectiveidea.com String maybeString = (String) null; if(maybeString != null)

    { String string = (String)maybeString; string.length(); } let
  53. @TTGonda collectiveidea.com val maybeString: String? = null 
 return maybeString?.length

    ?: 0 Elvis Operator ?:
  54. @TTGonda collectiveidea.com String maybeString = (String)null; 
 if(maybeString != null)

    {
 return maybeString.length();
 } else {
 return 0; } Elvis Operator ?:
  55. @TTGonda collectiveidea.com Delegation

  56. @TTGonda collectiveidea.com class CopyPrinter(copier: Copy, printer: Print) : Copy by

    copier, Print by printer interface Copy { fun copy(page: Page): Page } interface Print { fun print(page: Page) }
  57. @TTGonda collectiveidea.com public final class CopyPrinter implements Copy, Print {

    // $FF: synthetic field private final Copy $$delegate_0; // $FF: synthetic field private final Print $$delegate_1; public CopyPrinter(@NotNull Copy copier, @NotNull Print printer) { Intrinsics.checkParameterIsNotNull(copier, "copier"); Intrinsics.checkParameterIsNotNull(printer, "printer"); super(); this.$$delegate_0 = copier; this.$$delegate_1 = printer; } @NotNull public Page copy(@NotNull Page page) { Intrinsics.checkParameterIsNotNull(page, "page"); return this.$$delegate_0.copy(page); } public void print(@NotNull Page page) { Intrinsics.checkParameterIsNotNull(page, "page"); this.$$delegate_1.print(page); } } public interface Copy { @NotNull Page copy(@NotNull Page var1); } public interface Print { void print(@NotNull Page var1); }
  58. @TTGonda collectiveidea.com public final class CopyPrinter implements Copy, Print {

  59. @TTGonda collectiveidea.com // $FF: synthetic field private final Copy $$delegate_0;

    // $FF: synthetic field private final Print $$delegate_1; public final class CopyPrinter implements Copy, Print {
  60. @TTGonda collectiveidea.com public CopyPrinter( @NotNull Copy copier, @NotNull Print printer)

    { Intrinsics .checkParameterIsNotNull(copier, "copier"); Intrinsics .checkParameterIsNotNull(printer, "printer"); super(); this.$$delegate_0 = copier; this.$$delegate_1 = printer; } // $FF: synthetic field private final Copy $$delegate_0; // $FF: synthetic field private final Print $$delegate_1;
  61. @TTGonda collectiveidea.com @NotNull public Page copy(@NotNull Page page) { Intrinsics

    .checkParameterIsNotNull(page, "page"); return this.$$delegate_0.copy(page); } public void print(@NotNull Page page) { Intrinsics .checkParameterIsNotNull(page, "page"); this.$$delegate_1.print(page); } } super(); this.$$delegate_0 = copier; this.$$delegate_1 = printer; }
  62. @TTGonda collectiveidea.com public interface Copy { @NotNull Page copy(@NotNull Page

    var1); } public interface Print { void print(@NotNull Page var1); } public void print(@NotNull Page page) { Intrinsics .checkParameterIsNotNull(page, "page"); this.$$delegate_1.print(page); } }
  63. @TTGonda collectiveidea.com public final class CopyPrinter implements Copy, Print {

    // $FF: synthetic field private final Copy $$delegate_0; // $FF: synthetic field private final Print $$delegate_1; public CopyPrinter(@NotNull Copy copier, @NotNull Print printer) { Intrinsics.checkParameterIsNotNull(copier, "copier"); Intrinsics.checkParameterIsNotNull(printer, "printer"); super(); this.$$delegate_0 = copier; this.$$delegate_1 = printer; } @NotNull public Page copy(@NotNull Page page) { Intrinsics.checkParameterIsNotNull(page, "page"); return this.$$delegate_0.copy(page); } public void print(@NotNull Page page) { Intrinsics.checkParameterIsNotNull(page, "page"); this.$$delegate_1.print(page); } } public interface Copy { @NotNull Page copy(@NotNull Page var1); } public interface Print { void print(@NotNull Page var1); }
  64. @TTGonda collectiveidea.com Class Extensions

  65. @TTGonda collectiveidea.com Class Extensions TextUtils.isEmpty("hello");

  66. @TTGonda collectiveidea.com // StringExt.kt fun String.double(): String { return this

    + this }
  67. @TTGonda collectiveidea.com "hello".double() // -> “hellohello” // StringExt.kt fun String.double():

    String { return this + this }
  68. @TTGonda collectiveidea.com public final class StringExtKt { @NotNull public static

    final String double( @NotNull String $receiver) { Intrinsics .checkParameterIsNotNull($receiver, "$receiver"); return $receiver + $receiver; } }
  69. @TTGonda collectiveidea.com StringExtKt.double("hello"); // -> "hellohello" public final class StringExtKt

    { @NotNull public static final String double( @NotNull String $receiver) { Intrinsics .checkParameterIsNotNull($receiver, "$r return $receiver + $receiver; } }
  70. @TTGonda collectiveidea.com Lambdas

  71. @TTGonda collectiveidea.com fun firstNSquares(n: Int): Array<Int> = Array(n, { i

    -> i * i }) // firstNSquares(3) // -> [0, 1, 4]
  72. @TTGonda collectiveidea.com @NotNull public static final Integer[] firstNSquares(int n) {

    Integer[] result$iv = new Integer[n]; int i$iv = 0; int var3 = n - 1; if(i$iv <= var3) { while(true) { Integer var9 = Integer.valueOf(i$iv * i$iv); result$iv[i$iv] = var9; if(i$iv == var3) { break; } ++i$iv; } } return (Integer[])((Object[])result$iv); }
  73. @TTGonda collectiveidea.com @NotNull public static Integer[] firstNSquares(int n) { Integer[]

    resultArray = new Integer[n]; int i = 0; int max = n - 1; if(i <= max) { while(true) { Integer square = i * i; resultArray[i] = square; if(i == max) { break; } ++i; } } return resultArray; }
  74. @TTGonda collectiveidea.com fun firstNSquares(n: Int): Array<Int> = Array(n, { i

    -> square(i + 1) }) // firstNSquares(3) // -> [1, 4, 9]
  75. @TTGonda collectiveidea.com @NotNull public static Integer[] firstNSquares(int n) { Integer[]

    resultArray = new Integer[n]; int i = 0; int max = n - 1; if(i <= max) { while(true) { Integer square = square(i+1); resultArray[i] = square; if(i == max) { break; } ++i; } } return resultArray; }
  76. @TTGonda collectiveidea.com inline fun beforeAndAfter( startString: String, function: (string: String)

    -> String) { print("Before: $startString") val after = function(startString) print("After: $after") }
  77. @TTGonda collectiveidea.com inline fun beforeAndAfter( startString: String, function: (string: String)

    -> String) { print("Before: $startString") val after = function(startString) print("After: $after") } public inline fun <T, R> T.let(block: (T) -> R): R = block(this)
  78. @TTGonda collectiveidea.com inline fun beforeAndAfter( startString: String, function: (string: String)

    -> String) { print("Before: $startString") val after = function(startString) print("After: $after") }
  79. @TTGonda collectiveidea.com public final void beforeAndAfter( @NotNull String startString, @NotNull

    Function1 function) { Intrinsics .checkParameterIsNotNull(startString, "startString"); Intrinsics .checkParameterIsNotNull(function, "function"); String after = "Before: " + startString; System.out.print(after); after = (String)function.invoke(startString); String var4 = "After: " + after; System.out.print(var4); }
  80. @TTGonda collectiveidea.com public final void beforeAndAfter( @NotNull String startString, @NotNull

    Function1 function) { Intrinsics .checkParameterIsNotNull(startString, "startString"); Intrinsics .checkParameterIsNotNull(function, "function"); String after = "Before: " + startString; System.out.print(after); after = (String)function.invoke(startString); String var4 = "After: " + after; System.out.print(var4); } public interface Function1<in P1, out R> : Function<R> { public operator fun invoke(p1: P1): R }
  81. @TTGonda collectiveidea.com public final void beforeAndAfter( @NotNull String startString, @NotNull

    Function1 function) { Intrinsics .checkParameterIsNotNull(startString, "startString"); Intrinsics .checkParameterIsNotNull(function, "function"); String after = "Before: " + startString; System.out.print(after); after = (String)function.invoke(startString); String var4 = "After: " + after; System.out.print(var4); }
  82. @TTGonda collectiveidea.com fun example() { beforeAndAfter("hello", { string -> string

    + " world" }) } // Output “Before: hello" “After: hello world"
  83. @TTGonda collectiveidea.com public final void example() { String startString$iv =

    "hello"; String after$iv = "Before: " + startString$iv; System.out.print(after$iv); String string = (String)startString$iv; after$iv = (String)(string + " world"); string = "After: " + after$iv; System.out.print(string); }
  84. @TTGonda collectiveidea.com beforeAndAfter("hello", { string -> string + " world"

    }) 
 beforeAndAfter("hello", { it + " world" }) 
 beforeAndAfter("hello") { it + " world" } Lambda Parameters
  85. @TTGonda collectiveidea.com fun beforeAndAfter( startString: String, function: (string: String) ->

    String ) { print("Before: $startString") val after = function(startString) print("After: $after") }
  86. @TTGonda collectiveidea.com public final void example() { this.beforeAndAfter("hello", (Function1)null.INSTANCE); }

    // Output “Before: hello” “After: hello world”
  87. @TTGonda collectiveidea.com

  88. @TTGonda collectiveidea.com // Pseudocode for bytecode final class BytecodeClass extends

    Lambda implements Function1 { public void invoke(String string) { StringBuilder sb = new StringBuilder("hello"); sb.append(" world"); returnValue = sb.toString(); } static Function1 INSTANCE = new BytecodeClass(); }
  89. @TTGonda collectiveidea.com // Pseudocode for bytecode final class BytecodeClass extends

    Lambda implements Function1 { public void invoke(String string) { StringBuilder sb = new StringBuilder("hello"); sb.append(" world"); returnValue = sb.toString(); } static Function1 INSTANCE = new BytecodeClass(); } public final void example() { this.beforeAndAfter("hello", (Function1)null.INSTANCE); }
  90. @TTGonda collectiveidea.com // Pseudocode for bytecode final class BytecodeClass extends

    Lambda implements Function1 { public void invoke(String string) { StringBuilder sb = new StringBuilder("hello"); sb.append(" world"); returnValue = sb.toString(); } static Function1 INSTANCE = new BytecodeClass(); }
  91. @TTGonda collectiveidea.com Companion Objects Smart Casting Collection Functions Control Flow

    Structures Operator Overloading Named Parameters
  92. @TTGonda collectiveidea.com Menu > Tools > Kotlin > Show Kotlin

    Bytecode or CMD+SHFT+A and search “Show Kotlin Bytecode”
  93. @TTGonda collectiveidea.com Code > Convert Java File to Kotlin File

    or CMD+SHFT+A and search “Convert Java File to Kotlin File”
  94. @TTGonda collectiveidea.com DETOUR JAVA

  95. @TTGonda collectiveidea.com Thanks! Victoria Gonda www.victoriagonda.com @TTGonda