{"id":23064971,"url":"https://github.com/raymondxu/java-workshop","last_synced_at":"2025-08-15T10:31:42.249Z","repository":{"id":28415843,"uuid":"31930363","full_name":"raymondxu/java-workshop","owner":"raymondxu","description":"Intermediate Java workshop on variables, abstraction, and design patterns ☕","archived":false,"fork":false,"pushed_at":"2017-09-07T22:17:30.000Z","size":31,"stargazers_count":10,"open_issues_count":0,"forks_count":5,"subscribers_count":1,"default_branch":"master","last_synced_at":"2024-03-15T19:33:17.367Z","etag":null,"topics":["design-patterns","java","workshop"],"latest_commit_sha":null,"homepage":"","language":null,"has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"gpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/raymondxu.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2015-03-10T00:38:22.000Z","updated_at":"2024-03-15T19:33:17.368Z","dependencies_parsed_at":"2022-08-02T17:01:19.811Z","dependency_job_id":null,"html_url":"https://github.com/raymondxu/java-workshop","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/raymondxu%2Fjava-workshop","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/raymondxu%2Fjava-workshop/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/raymondxu%2Fjava-workshop/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/raymondxu%2Fjava-workshop/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/raymondxu","download_url":"https://codeload.github.com/raymondxu/java-workshop/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":229907430,"owners_count":18142715,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["design-patterns","java","workshop"],"created_at":"2024-12-16T04:24:43.956Z","updated_at":"2024-12-16T04:24:44.469Z","avatar_url":"https://github.com/raymondxu.png","language":null,"funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003ca id=\"top\"\u003e\u003c/a\u003e\n# Intermediate Java\n\nThis workshop is targeted towards people who have taken an introductory computer science course (e.g. 1004, 1006) or want a refresher in Java and software engineering fundamentals. I hope to elucidate concepts that may not have been crystal clear when you first learned them such as primitives and objects, static and final, and abstraction. Then we will dive into a few common object-oriented design patterns and explore why abstraction is so cool.\n\n## Table of Contents\n1. [Variables](#variables)\n\t1. [Primitives](#primitives)\n\t2. [Objects](#objects)\n\t3. [Equality](#equality)\n\t4. [Wrappers](#wrappers)\n\t5. [Access Modifiers](#accessmodifiers)\n\t6. [Static vs Final](#staticvsfinal)\n\t7. [Enums](#enums)\n2. [Abstraction](#abstraction)\n\t1. [Abstract Classes](#abstractclasses)\n\t2. [Interfaces](#interfaces)\n3. [Patterns](#patterns)\n\t1. [Strategy Pattern](#strategy)\n\t2. [Singleton Pattern](#singleton)\n\t3. [Composite Pattern](#composite)\n \n-------------------------\n \n\u003ca id=\"variables\"\u003e\u003c/a\u003e\n## 1.0 Variables\n\n\u003ca id=\"primitives\"\u003e\u003c/a\u003e\n### 1.1 Primitives\n\nPrimitives are data types that are defined by the language.\n\nIn Java there are 8 primitives:\n\n|  Primitive  |  Bytes  |\n|-------------|---------|\n| byte | 1 |\n| boolean | 1 |\n| char | 2 |\n| short | 2 |\n| int | 4 |\n| float | 4 |\n| double | 8 |\n| long | 8 |\n\nLet's look at some quick examples of primitive declarations:\n\n```java\nbyte a = -128;\nboolean b = true;\nchar c = 'q';\nshort d = 128;\nint e = 32768;\nfloat f = 9.81;\ndouble g = 3.14159265358979;\nlong h = 12345678901234567890;\n```\n\nPrimitives have independent states. Changing the value of one does not influence another.\n\n```java\nint a = 5;\nint b = a;\nb = 7;\nSystem.out.println(\"a is \" + a);\nSystem.out.println(\"b is \" + b);\n```\n\nGives us:\n\n```\na is 5\nb is 7\n```\n\nNow let's look at what happens when we do the same thing to objects.\n\n\u003ca id=\"objects\"\u003e\u003c/a\u003e\n### 1.2 Objects\n\nHere's a simple class declaration:\n\n```java\npublic class Car\n{\n\tpublic int year;\n  \n\tpublic Car(int y)\n\t{\n\t\tyear = y;\n\t}\n}\n```\n\nWhat happens when we execute the following piece of code?\n\n```java\npublic static void main(String[] args)\n{\n\tCar a = new Car(1996);\n\tCar b = a;\n\tb.year = 2015;\n\t  \n\tSystem.out.println(\"a's year is \" + a.year);\n\tSystem.out.println(\"b's year is \" + b.year);\n}\n```\n\nWe get:\n\n```\na's year is 2015\nb's year is 2015\n```\n\nWhy?\n\nThe answer lies in \u003cb\u003ereferences\u003c/b\u003e.\n\nWhen we instantiate an object, we create a reference to that object in memory. A value of a reference is the location of the object.\n\n```java\nCar a = new Car();\n```\n\nThis line of code calls the default constructor of the Car class to instantiate a new reference of a Car object. `a` is \u003cb\u003enot\u003c/b\u003e actually the Car object, it is a \u003cb\u003ereference\u003c/b\u003e to the Car object.\n\nSo when we tell `a` to execute a method, we are actually telling whatever `a` points at to execute the method. Since multiple variables can reference the same object, it sometimes gets confusing. Let's look back at our code above:\n\n```java\npublic static void main(String[] args)\n{\n \tCar a = new Car(1996);\n \tCar b = a;\n \tb.year = 2015;\n \t\n \tSystem.out.println(\"a's year is \" + a.year);\n \tSystem.out.println(\"b's year is \" + b.year);\n}\n```\n\n`a` is a reference to a Car object that has a year of 2015.\n`b` is a reference to that same Car object. You don't see the keyword `new`, so you know that there are no other Car objects in play.\nWhen we change the year of `b`, we change the year of the Car object that `a` and `b` both refer to. This is why we get:\n\n```\na's year is 2015\nb's year is 2015\n```\n\n\u003ca id=\"equality\"\u003e\u003c/a\u003e\n### 1.3 Equality\n\nOn a related note, how do you compare if two objects are equal?\n\n```java\nCar a = new Car(1996);\nCar b = new Car(1996);\nSystem.out.println(a == b);\n```\n\nThis is \u003cb\u003ewrong\u003c/b\u003e. The `==` operator in Java tells you if two references are equal. It does not tell you if the objects have the same instance variable values or any other comparison metric. So for most purposes, when comparing object equality, we use the `equals()` method. Let's looks at the documentation from the Java API:\n\n\u003e public boolean equals(Object obj)\n\u003e \n\u003e Indicates whether some other object is \"equal to\" this one.\n\u003e The equals method implements an equivalence relation on non-null object references:\n\u003e \n\u003e The equals method implements an equivalence relation on non-null object references:\n\u003e \n\u003e It is reflexive: for any non-null reference value x, x.equals(x) should return true.\n\u003e It is symmetric: for any non-null reference values x and y, x.equals(y) should return true if and only if y.equals(x) returns true.\n\u003e It is transitive: for any non-null reference values x, y, and z, if x.equals(y) returns true and y.equals(z) returns true, then x.equals(z) should return true.\n\u003e It is consistent: for any non-null reference values x and y, multiple invocations of x.equals(y) consistently return true or consistently return false, provided no information used in equals comparisons on the objects is modified.\n\u003e For any non-null reference value x, x.equals(null) should return false.\n\u003e \n\u003e The equals method for class Object implements the most discriminating possible equivalence relation on objects; that is, for any non-null reference values x and y, this method returns true if and only if x and y refer to the same object (x == y has the value true).\n\u003e \n\u003e Note that it is generally necessary to override the hashCode method whenever this method is overridden, so as to maintain the general contract for the hashCode method, which states that equal objects must have equal hash codes.\n\u003e \n\u003e Parameters:\n\u003e obj - the reference object with which to compare.\n\u003e Returns:\n\u003e true if this object is the same as the obj argument; false otherwise.\n\nThis means that when we write our own classes and want to include equality comparison capabilities, we need to override the `equals()` method and write our own equality metric. For example, our Car class could use:\n\n```java\npublic boolean equals(Object obj)\n{\n    return this.year == ((Car)(obj)).year;\n}\n```\n\nThis is a simple example, but note that if our Car class had other instance variables like color and model, we could define two Cars as equal if all of their qualities were equal, or just a few.\n\nBut wait! Why do we use `==` when we compare years?\n\nRemember that `year` is an `int` which is a primitive. So this means we can't even invoke a method on a `year`, so using `equals()` is out of the question. For primitives, `==` compares values.\n\n```java\nint a = 5;\nint b = 5;\nSystem.out.println(a == b);\n```\n\nGives us:\n\n```\ntrue\n```\n\nA general rule of thumb is `equals()` for objects, and `==` for primitives.\n\n\u003cb\u003eTwo objects that are equal must return the same hash code.\u003c/b\u003e This is a contract between `equals()` and `hashCode()`. `equals()` and `hashCode()` are both methods in `Object`, which every class implicitly inherits from. By default, `hashCode()` returns the memory location of an object, and `equals()` compares if the `hashCode()` of two objects is the same. When we overwrite either of these two methods, we have to make sure that the contract is still upheld. \n\n\u003ca id=\"wrappers\"\u003e\u003c/a\u003e\n### 1.4 Wrappers\n\nSometimes we need objects. For example, when we want to create an ArrayList, we have to specify what type of object will be stored in the ArrayList.\n\n```java\nList\u003cInteger\u003e myList = new ArrayList\u003cInteger\u003e();\n```\n\nBut why can't we use `int`? Why do we have to use `Integer`?\n\nBecause `int` is not an object, and the generic `E` in `ArrayList\u003cE\u003e` must be a class.\n\n`Integer` is an example of a wrapper class: a class that wraps a primitive into an object.\n\nJava provides built-in \u003cb\u003eprimitive wrappers\u003c/b\u003e for all 8 primitive types.\n\n| Primitive  |  Wrapper class  |\n|------------|-----------------|\n| byte | Byte |\n| boolean | Boolean |\n| char | Character |\n| short | Short |\n| int | Integer |\n| float | Float |\n| double | Double |\n| long | Long |\n\nThis seems a little complicated at first, but note that Java \u003cb\u003eautoboxes\u003c/b\u003e for us. This means that we don't have to explicitly convert between wrapper objects and primitives. We can construct an ArrayList of Integer objects and then add the Integer objects just like we add primitives.\n\n```java\nArrayList\u003cInteger\u003e myList = new ArrayList\u003cInteger\u003e();\nint sum = 0;\n\nfor (int i = 0; i \u003c myList.size(); i++)\n{\n\tsum += myList.get(i);\n}\n```\n\n\n\u003ca id=\"accessmodifiers\"\u003e\u003c/a\u003e\n### 1.5 Access Modifiers\n\nAll instance variables have an access modifier. You may be familiar with `public` and `private`:\n\n```java\npublic Car a;\nprivate Car b;\n```\n\nThere are two more you may not have heard of: `protected` and `packaged`.\n\n```java\nprotected Car c;\nCar d;\n```\n\nNote that the `packaged` modifier is represented by the absence of a modifier.\n\n\u003ci\u003eNote that that the absence of a modifier in interfaces means `public` by virtue of the role of interfaces. Later in this talk we will discuss interfaces and abstraction at a deeper level.\u003c/i\u003e\n\nThis chart elegantly displays the distinctions between the four access modifiers. A checked box indicates that the variable can be accessed from that scope. For example, a `private` variable can only be accessed from within that class--other classes in the package, its subclasses, and other classes outside its package cannot access it.\n\n| Modifier | Class | Package | Subclass | World |\n|----------|:-----:|:-------:|:--------:|:-----:|\n| public | ✓ | ✓ | ✓ | ✓ |\n| protected | ✓ | ✓ | ✓ |  |\n| packaged | ✓ | ✓ |  |  |\n| private | ✓ |  |  |  |\n\n\u003ca id=\"staticvsfinal\"\u003e\u003c/a\u003e\n### 1.6 Static vs Final\n\n`static` is used to describe fields and methods that are associated with a class, not instances of a class.\n\nFor example, in our Car example above, each Car instance has its own instance variables that describe its color or year. These fields are not `static`.\n\nAn example of a static method is `Math.max()` or `Math.abs()` and an example of a static variable is `Math.PI`. Notice how we never create an instance of the `Math` class?\n\n`Math.PI` is also a `final` variable. `final` is a keyword used to describe things that are constant. It can describe fields, methods, and classes.\n\nA `final` field is a constant that does not change. A `final` variable can only be initialized once. If the variable holds a reference to an object it cannot ever refer to another object. The contents of the object can change, but the variable will always \"point\" to that exact instance. For example, if we had a `final ArrayList\u003cInteger\u003e list`, we could `add()` and `remove()` elements from the ArrayList, but we can't do `list = new ArrayList\u003cInteger\u003e()`.\n\nA `final` method cannot be overridden by a subclass.\n\nA `final` class cannot be extended (no subclasses allowed).\n\n\u003ca id=\"enums\"\u003e\u003c/a\u003e\n### 1.7 Enums\n\nSay we want to represent different coins like pennies, nickels, dimes, and quarters. There are a lot of approaches to this design dilemma. One way is to simply define a `Coin` class with a field to store the value.\n\n```java\npublic class Coin\n{\n\tint value;\n\t\n\tpublic Coin(int value)\n\t{\n\t\tthis.value = value;\n\t}\n}\n```\n\nThis is pretty bad. Why?\n\nHow can we do better?\n\nWe can define the denominations using static final ints:\n\n```java\npublic class CoinDenomination\n{\n\tpublic static final int PENNY = 1;\n\tpublic static final int NICKEL = 5;\n\tpublic static final int DIME = 10;\n\tpublic static final int QUARTER = 25;\n}\n```\n\nNow we can create coins like this:\n\n```java\nCoin c = new Coin(CoinDenomination.QUARTER);\n```\n\nCool! But how can we do better?\n\nEnums!\n\n```java\npublic enum Coin { PENNY, NICKEL, DIME, QUARTER };\n```\n\nIn Java, this syntax automatically creates the static final constants PENNY, NICKEL, DIME, and QUARTER. Let's associate values with them!\n\n```java\npublic enum Coin\n{\n\tPENNY(1), NICKEL(5), DIME(10), QUARTER(25)\n\t\n\tprivate int value;\n\t\n\tprivate Coin(int value)\n\t{\n\t\tthis.value = value;\n\t}\n};\n```\n\nNow we can create coins like this:\n\n```java\nCoin c = Coin.QUARTER;\n```\n\nEnums are essentially mini-classes that represent a set of constants. They can optionally have fields and methods. Some other basic examples of enum use cases are to represent the days of the week, planets in our solar system, and chess pieces.\n\nNotes:\nThe enum constructor must be `private` to preserve type-safety.\nSince enum constants are `final`, we compare them using `==`.\n\n\u003ca id=\"abstraction\"\u003e\u003c/a\u003e\n## 2.0 Abstraction\n\nAbstraction is a programming ideology that involves extracting physical implementation from method signatures. Abstract classes and interfaces are ways to achieve abstraction. Abstract classes and interfaces are similar in many ways so they can be tough to understand at first. Let's try and clear that up.\n\n\u003ca id=\"abstractclasses\"\u003e\u003c/a\u003e\n### 2.1 Abstract Classes\n\nAbstract classes can contain instance variables, abstract methods, and concrete methods. If a class extends an abstract class, it must implement all abstract methods defined in its parent--unless it itself is an abstract class. Abstract classes cannot be instantiated.\n\nSemantically, abstract classes define a close relation between the abstract class and its subclasses. For example, you could define an abstract class called `GeometricShape`.\n\n```java\nabstract class GeometricShape\n{\n\tColor col;\n\n\tColor getColor()\n\t{\n\t\treturn col;\n\t}\n\t\n\tabstract double calculateArea();\n}\n```\n\nJust like you can't really draw just a generic geometric shape, you can't instantiate a `GeometricShape` object because the class is abstract. Let's define some subclasses:\n\n```java\nclass Square extends GeometricShape\n{\n\tdouble edgeLength;\n\n\tdouble calculateArea()\n\t{\n\t\treturn edgeLength * edgeLength;\n\t}\n}\n\nclass Circle extends GeometricShape\n{\n\tdouble radius;\n\t\n\tdouble calculateArea()\n\t{\n\t\treturn Math.PI * Math.pow(radius, 2);\n\t}\n}\n```\n\n\u003ca id=\"interfaces\"\u003e\u003c/a\u003e\n### 2.2 Interfaces\n\nInterfaces cannot have instance variables and cannot have implementations of methods. You can think of interfaces as templates for classes that share a common trait. When a class implements an interface, it must implement all of the methods declared in that interface. Like abstract classes, interfaces cannot be instantiated.\n\nFor example, the `Comparable` interface only defines the method `compareTo(T o)`. The Oracle documentation describes its purpose: \"Compares this object with the specified object for order. Returns a negative integer, zero, or a positive integer as this object is less than, equal to, or greater than the specified object.\" So if we have a class that implements the `Comparable` interface, it \u003cb\u003emust\u003c/b\u003e provide an implementation for the `compareTo(T o)` method. In this sense, interfaces are also \u003cb\u003econtracts\u003c/b\u003e between the high-level and the low-level details of the program.\n\nLet's say we have a `Speakable` interface with the method `speak()`.\n\n```java\npublic interface Speakable()\n{\n\tvoid speak();\n}\n```\n\nNow we can let `Human`, `Dog`, and `Car` all implement `Speakable`.\n\n```java\npublic class Human implements Speakable\n{\n\tpublic void speak()\n\t{\n\t\tSystem.out.println(\"Hello!\");\n\t}\n}\n\npublic class Dog implements Speakable\n{\n\tpublic void speak()\n\t{\n\t\tSystem.out.println(\"Bark!\");\n\t}\n}\n\npublic class Train implements Speakable\n{\n\tpublic void speak()\n\t{\n\t\tSystem.out.println(\"Choo choo!\");\n\t}\n}\n```\n\nWe can now aggregate them!\n\n```java\nList\u003cSpeakable\u003e thingsThatCanSpeak = new ArrayList\u003cSpeakable\u003e();\nthingsThatCanSpeak.add(new Human());\nthingsThatCanSpeak.add(new Dog());\nthingsThatCanSpeak.add(new Train());\n```\n\nWatch this:\n\n```java\nfor (Speakable speaker : thingsThatCanSpeak)\n{\n\tthingsThatCanSpeak.get(i).speak();\n}\n```\n\nOutputs:\n\n```\nHello!\nBark!\nChoo choo!\n```\n\n\u003ci\u003eWe can use the Java \"foreach loop\" or \"enhanced for loop\" here because `ArrayList` is an implementation of the `List` interface which implements `Iterable`.\u003c/i\u003e\n\nA class can implement multiple interfaces but only extend one class. Additionally, all methods in an interface are public, and all fields in an interface are public, static, and final. And unlike the fact that abstract classes should be closely related to their subclasses, interfaces are used when the implementing classes are unrelated to each other.\n\n\u003ca id=\"patterns\"\u003e\u003c/a\u003e\n## 3.0 Patterns\n\n\u003ca id=\"strategy\"\u003e\u003c/a\u003e\n### 3.1 Strategy Pattern\n\nThe strategy pattern enables easy interchangability of parts. We define a grouping of parts (interface) and then encapsulate each specific part (classes that implement the interface). This way, when a part is needed, any one of the parts can be used and the program will run smoothly.\n\n```java\npublic interface RandomNumberGenerator\n{\n\tint roll();\n}\n\npublic class Die implements RandomNumberGenerator\n{\n\tpublic int roll()\n\t{\n\t\treturn (int)(Math.random() * 6) + 1;\n\t}\n}\n\npublic class RiggedDie implements RandomNumberGenerator\n{\n\tpublic int roll()\n\t{\n\t\treturn 4;\n\t}\n}\n\npublic class Casino\n{\n\tprivate RandomNumberGenerator x;\n\t\n\tpublic Casino(RandomNumberGenerator x)\n\t{\n\t\tthis.x = x;\n\t}\n\t\n\tpublic void play()\n\t{\n\t\t// ...\n\t\tint outcome = x.roll();\n\t\t// ...\n\t}\n}\n```\n\n\u003ca id=\"singleton\"\u003e\u003c/a\u003e\n### 3.2 Singleton Pattern\n\nThe singleton pattern only allows there to be one instance of a class. There are a few ways to achieve this but here is a simple way:\n\n```java\npublic class Dictator\n{\n\tprivate static final Dictator INSTANCE = new Dictator();\n\tprivate Dictator() {}\n\t\n\tpublic static Dictator getInstance()\n\t{\n\t\treturn INSTANCE;\n\t}\n}\n```\n\nAs you can see, the concepts `static`, `final`, and `private` that we learned earlier all play a role in this pattern.\n\nAnother interesting implemention of the singleton pattern is based off of the initialization-on-demand holder idiom. This implementation takes advantage of the way the JVM initializes classes. Let's look at a simple example:\n\n```java\npublic class Dictator\n{\n\tprivate Dictator() {}\n\n\tprivate static class LazyHolder\n\t{\n\t\tprivate static final Dictator INSTANCE = new Dictator();\n\t}\n\n\tpublic static Dictator getInstance()\n\t{\n\t\treturn LazyHolder.INSTANCE;\n\t}\n}\n```\n\nBecause `LazyHolder` is a static inner class of `Dictator`, it is not initialized in the initialization phase of the JVM. It is only when the static `getInstance()` method is first invoked that Java realizes that it needs to initialize `LazyHolder` and does so.\n\nThis pattern results in a thread-safe singleton instance because Java initialization is guaranteed to be serial. That is, no synchronization overhead needs to be added to this class to preserve the singleton property. Additionally, lazy loading adds a bit of efficiency to the overall program since we only initialize `LazyHolder` when it is immediately needed.\n\n\n\u003ca id=\"composite\"\u003e\u003c/a\u003e\n### 3.3 Composite Pattern\n\nThe composite pattern allows for flexible aggregation of different objects that share a common trait. This common trait is embodied by an interface.\n\n```java\npublic interface Packageable\n{\n\tint getWeight();\n}\n\npublic class Item implements Packageable\n{\n\tprivate int weight;\n\n\tpublic Item(int weight)\n\t{\n\t\tthis.weight = weight;\n\t}\n\n\tpublic int getWeight()\n\t{\n\t\treturn weight;\n\t}\n}\n\npublic class Box implements Packageable\n{\n\tprivate List\u003cPackageable\u003e contents = new ArrayList\u003cPackageable\u003e();\n\t\n\tpublic int getWeight()\n\t{\n\t\tint sum = 0;\n\t\tfor (Packageable p : contents)\n\t\t{\n\t\t\tsum += p.getWeight();\n\t\t}\n\t\treturn sum;\n\t}\n}\n```\n\nWhy is this cool?\n\nNot only can we can put items into boxes, but we can also put boxes into boxes! And we can query the weight of any of the Packageable items because they all have a `getWeight()` method!\n\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fraymondxu%2Fjava-workshop","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fraymondxu%2Fjava-workshop","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fraymondxu%2Fjava-workshop/lists"}