Java Lambda表示式應用介紹,幫助大家快速掌握Lambda
JAVA中LAMBDA的應用
本文主要著重講解LAMBDA表示式的用法,關於LAMBDA的實現可能會在後續的文章中講解。本文內容基本基於一個公眾號的內容,在此基礎上進行精簡。如果有興趣深入瞭解的同學請看連結:關於Java Lambda表示式看這一篇就夠了。
Lambda表示式與匿名內部類
之所以在java中用lambda表示式其實就是可以用它來簡化代替一些匿名內部類。但是Lambda不能代替所有的匿名內部類。只能用來取代函式介面的簡寫。
我們先來看一下Java7之前如果想用匿名內部類實現一個執行緒是怎麼寫的。
Thread t = new Thread(
new Runnable(){
@Override
public void run(){
//doSomething
}
}
);
上面的程式碼就是我們需要給Thread類傳入一個Runnable的物件。但是這樣看起來很繁重。所以Java8通過引用Lambda表示式可以簡化為如下:
Thread t = new Thread(()->{//doSomething})
可以看到使用Lambda可以省略類名和方法名。如果過載的方法裡面只有一行程式碼還可以把{}省略。
上面講的是不帶引數的寫法,帶引數的寫法同樣簡單比如我們想排序一個Collection物件在Java8之前我們需要:
List<Integer> list = Arrays.asList(2,3,5,2,3);
Collections.sort(list, new Comparator<Integer>(){
@Override
public int compare(Integer a, Integer b){
return a-b;
}
});
如果用Lambda的話如下:
List<Integer> list = Arrays.asList(2,3,5,2,3);
Collections.sort(list,(a,b)->a-b);
Lambda可以省略引數型別主要是因為javac的型別推斷機制。當有推斷失敗的時候就要手動的指定型別了。
自定義介面
我們看到Lambda是代替介面的實現。可以被Lambda實現的介面我們叫函式介面。這種介面要保證介面裡面只有一個抽象方法。那麼我們怎麼去編寫自己想要的介面呢?其實很簡單,具體如下:
@FunctionalInterface
public interface FInterface<T>{
void accept(T t);
}
這裡面@FunctionalInterface可以幫我們判斷我們所寫的介面是不是滿足函式介面。
注意this
雖然我們說Lambda是代替了匿名內部類,但是Lambda的實現和匿名內部類的實現是不一樣的。具體情況你可以自己寫好程式碼之後通過javac看一下生成的class檔案有什麼不同或者看我在開頭附上的連結。
在匿名內部類使用this指向的是類的本身,而用Lambda表示式this指向的還是外部的類。
Lambda與Collections
在Java8中新增類java.util.function包。裡面包含了常用的函式介面。我們看一下各個類中都新增了什麼方法:
介面名|Java8新增方法
專案 | Value |
---|---|
Collection | removeIf(), spliterator(), stream(), parallelStream(), forEach() |
List | replaceAll(), sort() |
Map | getOrDefault(), forEach(), replaceAll(), putIfAbsent(), remove(), replace(), computeIfAbsent(), computeIfPresent(), compute(), merge() |
下面我們來逐一的學習這些方法。
Collection中的新方法
1. forEach()
首先是forEach方法。我們先看一下forEach方法的原始碼:
default void forEach(Consumer<? super T> action) {
Objects.requireNonNull(action);
for (T t : this) {
action.accept(t);
}
}
可以看到我們需要傳給forEach方法一個Consumer實體物件。那Consumer是什麼呢?
@FunctionalInterface
public interface Consumer<T> {
/**
* Performs this operation on the given argument.
*
* @param t the input argument
*/
void accept(T t);
/**
* Returns a composed {@code Consumer} that performs, in sequence, this
* operation followed by the {@code after} operation. If performing either
* operation throws an exception, it is relayed to the caller of the
* composed operation. If performing this operation throws an exception,
* the {@code after} operation will not be performed.
*
* @param after the operation to perform after this operation
* @return a composed {@code Consumer} that performs in sequence this
* operation followed by the {@code after} operation
* @throws NullPointerException if {@code after} is null
*/
default Consumer<T> andThen(Consumer<? super T> after) {
Objects.requireNonNull(after);
return (T t) -> { accept(t); after.accept(t); };
}
}
可以看到Consumer就是我們之前提到的函式介面。所以我們在forEach裡面用Lambda表示式可以實現這個介面。這樣我們就可以輕鬆的遍歷一遍list裡面的元素了。具體如下:
List<Integer> list = Arrays.asList(1,2,3,4,5,6);
list.forEach(a->System.out.println(a));
2. removeIf()
這個方法作用是刪除滿足filter指定條件的元素。依然是先看一下原始碼:
default boolean removeIf(Predicate<? super E> filter) {
Objects.requireNonNull(filter);
boolean removed = false;
final Iterator<E> each = iterator();
while (each.hasNext()) {
if (filter.test(each.next())) {
each.remove();
removed = true;
}
}
return removed;
}
邏輯很簡單就是滿足這個filter條件就remove這個元素。而這個filter是一個Predicate介面的例項。我們看一下Predicate介面:
@FunctionalInterface
public interface Predicate<T> {
/**
* Evaluates this predicate on the given argument.
*
* @param t the input argument
* @return {@code true} if the input argument matches the predicate,
* otherwise {@code false}
*/
boolean test(T t);
/**
* Returns a composed predicate that represents a short-circuiting logical
* AND of this predicate and another. When evaluating the composed
* predicate, if this predicate is {@code false}, then the {@code other}
* predicate is not evaluated.
*
* <p>Any exceptions thrown during evaluation of either predicate are relayed
* to the caller; if evaluation of this predicate throws an exception, the
* {@code other} predicate will not be evaluated.
*
* @param other a predicate that will be logically-ANDed with this
* predicate
* @return a composed predicate that represents the short-circuiting logical
* AND of this predicate and the {@code other} predicate
* @throws NullPointerException if other is null
*/
default Predicate<T> and(Predicate<? super T> other) {
Objects.requireNonNull(other);
return (t) -> test(t) && other.test(t);
}
/**
* Returns a predicate that represents the logical negation of this
* predicate.
*
* @return a predicate that represents the logical negation of this
* predicate
*/
default Predicate<T> negate() {
return (t) -> !test(t);
}
/**
* Returns a composed predicate that represents a short-circuiting logical
* OR of this predicate and another. When evaluating the composed
* predicate, if this predicate is {@code true}, then the {@code other}
* predicate is not evaluated.
*
* <p>Any exceptions thrown during evaluation of either predicate are relayed
* to the caller; if evaluation of this predicate throws an exception, the
* {@code other} predicate will not be evaluated.
*
* @param other a predicate that will be logically-ORed with this
* predicate
* @return a composed predicate that represents the short-circuiting logical
* OR of this predicate and the {@code other} predicate
* @throws NullPointerException if other is null
*/
default Predicate<T> or(Predicate<? super T> other) {
Objects.requireNonNull(other);
return (t) -> test(t) || other.test(t);
}
/**
* Returns a predicate that tests if two arguments are equal according
* to {@link Objects#equals(Object, Object)}.
*
* @param <T> the type of arguments to the predicate
* @param targetRef the object reference with which to compare for equality,
* which may be {@code null}
* @return a predicate that tests if two arguments are equal according
* to {@link Objects#equals(Object, Object)}
*/
static <T> Predicate<T> isEqual(Object targetRef) {
return (null == targetRef)
? Objects::isNull
: object -> targetRef.equals(object);
}
/**
* Returns a predicate that is the negation of the supplied predicate.
* This is accomplished by returning result of the calling
* {@code target.negate()}.
*
* @param <T> the type of arguments to the specified predicate
* @param target predicate to negate
*
* @return a predicate that negates the results of the supplied
* predicate
*
* @throws NullPointerException if target is null
*
* @since 11
*/
@SuppressWarnings("unchecked")
static <T> Predicate<T> not(Predicate<? super T> target) {
Objects.requireNonNull(target);
return (Predicate<T>)target.negate();
}
}
可以看到我們這個介面裡面只有一個test函式需要實現。所以我們依然可以用Lambda表示式實現:
List<Integer> list = new ArrayList<>(Arrays.asList(1,2,3,4,5,6));
list.removeIf(a -> a>1);
我們還可以看到在Predicate介面裡面還有一些default函式,這些函式可以用來配合多個filter一起使用。這裡就不再贅述。大家自己試一試就可以明白,不是很難理解。
3. replaceAll()
這個方法就是對每個元素進行一個操作,之後操作的返回結果替換原來的元素。方法原始碼:
default void replaceAll(UnaryOperator<E> operator) {
Objects.requireNonNull(operator);
final ListIterator<E> li = this.listIterator();
while (li.hasNext()) {
li.set(operator.apply(li.next()));
}
}
邏輯依然很簡單。我們看一下UnaryOperator這個介面:
@FunctionalInterface
public interface UnaryOperator<T> extends Function<T, T> {
static <T> UnaryOperator<T> identity() {
return t -> t;
}
}
我們發現這個介面並沒有apply方法。其實這個方法是放在了Function介面裡面:
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
Objects.requireNonNull(before);
return (V v) -> apply(before.apply(v));
}
default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
Objects.requireNonNull(after);
return (T t) -> after.apply(apply(t));
}
static <T> Function<T, T> identity() {
return t -> t;
}
}
我們發現這兩個介面裡面都有一個identity靜態方法。這個方法其實就是返回一個apply函式為返回輸入本身的Function物件。所以有的時候我們可以用identity()代替t->t。
4. sort()
這個就不多做講解了,之前已經給出了例子,大家返回上面看一下就可以了。
5. spliterator()
這個方法和Lambda沒什麼關係,有興趣的同學可以看一下這個部落格:Spliterator深入解讀
6. Stream和parallelStream
這兩個方法會在後面詳細解釋,所以暫時先跳過。
Map中的新方法
在這一部分,我基於HashMap來講解新增的函式。如果對HashMap的實現沒有基礎的同學請自己看一下HashMap的實現原理。
1. forEach()
先上forEach()原始碼:
@Override
public void forEach(BiConsumer<? super K, ? super V> action) {
Node<K,V>[] tab;
if (action == null)
throw new NullPointerException();
if (size > 0 && (tab = table) != null) {
int mc = modCount;
for (Node<K,V> e : tab) {
for (; e != null; e = e.next)
action.accept(e.key, e.value);
}
if (modCount != mc)
throw new ConcurrentModificationException();
}
}
基本上就是用兩個for迴圈遍歷所有的Node。之後把每個node的key和value放入BiConsumer裡面的accept方法。BiConsumer的原始碼就不放了,感興趣的同學可以自己看一下。
使用forEach的程式碼如下:
HashMap<Integer, String> map = new HashMap<>();
map.put(1, "one");
map.put(2, "two");
map.put(3, "three");
map.forEach((k, v) -> System.out.println(k + "=" + v));
2. getOrDefault()和putIfAbsent()
這兩個方法跟Lambda沒什麼關係就不詳細介紹了,但是這個方法一定掌握,因為經常會用到它,所以在這裡提及一下。
3. replaceAll()
先看原始碼:
public void replaceAll(BiFunction<? super K, ? super V, ? extends V> function) {
Node<K,V>[] tab;
if (function == null)
throw new NullPointerException();
if (size > 0 && (tab = table) != null) {
int mc = modCount;
for (Node<K,V> e : tab) {
for (; e != null; e = e.next) {
e.value = function.apply(e.key, e.value);
}
}
if (modCount != mc)
throw new ConcurrentModificationException();
}
}
這個方法就是根據BiFunction裡面apply函式的返回值更改對應的value。簡單的使用就是:
HashMap<Integer, String> map = new HashMap<>();
map.put(1, "one");
map.put(2, "two");
map.put(3, "three");
map.replaceAll((k, v) -> v.toUpperCase());
4. merge()
原始碼:
@Override
public V merge(K key, V value,
BiFunction<? super V, ? super V, ? extends V> remappingFunction) {
if (value == null || remappingFunction == null)
throw new NullPointerException();
int hash = hash(key);
Node<K,V>[] tab; Node<K,V> first; int n, i;
int binCount = 0;
TreeNode<K,V> t = null;
Node<K,V> old = null;
if (size > threshold || (tab = table) == null ||
(n = tab.length) == 0)
n = (tab = resize()).length;
if ((first = tab[i = (n - 1) & hash]) != null) {
if (first instanceof TreeNode)
old = (t = (TreeNode<K,V>)first).getTreeNode(hash, key);
else {
Node<K,V> e = first; K k;
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))) {
old = e;
break;
}
++binCount;
} while ((e = e.next) != null);
}
}
if (old != null) {
V v;
if (old.value != null) {
int mc = modCount;
v = remappingFunction.apply(old.value, value);
if (mc != modCount) {
throw new ConcurrentModificationException();
}
} else {
v = value;
}
if (v != null) {
old.value = v;
afterNodeAccess(old);
}
else
removeNode(hash, key, null, false, true);
return v;
} else {
if (t != null)
t.putTreeVal(this, tab, hash, key, value);
else {
tab[i] = newNode(hash, key, value, first);
if (binCount >= TREEIFY_THRESHOLD - 1)
treeifyBin(tab, hash);
}
++modCount;
++size;
afterNodeInsertion(true);
return value;
}
}
原始碼稍微有點兒長。首先merge需要提供三個引數。k,v和BiFunction。之後根據提供的k找到在hashmap裡面對應的value我們叫v2。如果v2不存在就把v和k關聯。如果v2存在則把v2和v放到BiFunction的apply函式裡面。返回的結果作為新的v。
使用如下:
map.merge(key, value, (v1, v2) -> v1+v2);
5. compute()
原始碼:
@Override
public V compute(K key,
BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
if (remappingFunction == null)
throw new NullPointerException();
int hash = hash(key);
Node<K,V>[] tab; Node<K,V> first; int n, i;
int binCount = 0;
TreeNode<K,V> t = null;
Node<K,V> old = null;
if (size > threshold || (tab = table) == null ||
(n = tab.length) == 0)
n = (tab = resize()).length;
if ((first = tab[i = (n - 1) & hash]) != null) {
if (first instanceof TreeNode)
old = (t = (TreeNode<K,V>)first).getTreeNode(hash, key);
else {
Node<K,V> e = first; K k;
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))) {
old = e;
break;
}
++binCount;
} while ((e = e.next) != null);
}
}
V oldValue = (old == null) ? null : old.value;
int mc = modCount;
V v = remappingFunction.apply(key, oldValue);
if (mc != modCount) { throw new ConcurrentModificationException(); }
if (old != null) {
if (v != null) {
old.value = v;
afterNodeAccess(old);
}
else
removeNode(hash, key, null, false, true);
}
else if (v != null) {
if (t != null)
t.putTreeVal(this, tab, hash, key, v);
else {
tab[i] = newNode(hash, key, v, first);
if (binCount >= TREEIFY_THRESHOLD - 1)
treeifyBin(tab, hash);
}
modCount = mc + 1;
++size;
afterNodeInsertion(true);
}
return v;
}
其實和merge很像。區別就是這次放到BiFunction的apply函式裡面的引數是key。
可以用compute方法實現merge,程式碼如下:
map.compute(key, (k,v) -> v==null ? newValue : v.concat(newValue));
6. computeIfAbsent() 和 computeIfPresent()
這兩個方法和compute有一點點區別。其實你們看名字就明白了。一個是如果key存在再執行apply方法,之後把k和v放入到map中。另一個就是當map中不存在key的時候呼叫apply方法,之後更改對應的value值。
Stream API(很牛X)
為什麼我們要用Stream API呢?主要原因有亮點
- 簡潔(後面看程式碼就能體會到)
- 對於多核友好。呼叫parallel()方法就可以。
這個圖片展示了java中的四種stream,他們均繼承自BaseStream類。IntStream,LongStream,DoubleStream分別對應三種基本型別。Stream對應剩餘的型別。
大部分情況我們可以通過呼叫Collection.stream()得到。但是他與collection本身還是有區別的。具體區別可以看一下原博,我就不細講了。
1. forEach()
對於這個方法其實和collection裡面的forEach很像,我們直接看一下程式碼就可以了。
Stream<String> stream = Stream.of("I", "love", "you", "too");
stream.forEach(str -> System.out.println(str));
2. filter()
故名思議,過濾。就是把符合條件的元素留下。看一下這個抽象方法:
Stream<T> filter(Predicate<? super T> predicate);
可以看到我們就是傳進去一個Predicate介面實現。這個幾口我們之前已經講過。用法如下:
Stream<String> stream= Stream.of("I", "love", "you", "too");
stream.filter(str -> str.length()==3)
.forEach(str -> System.out.println(str));
3. distinct()
去除重複的元素。用法如下:
Stream<String> stream= Stream.of("I", "love", "you", "too", "too");
stream.distinct()
.forEach(str -> System.out.println(str));
4. sorted()
排序。傳入一個Comparator。用法如下
Stream<String> stream= Stream.of("I", "love", "you", "too");
stream.sorted((str1, str2) -> str1.length()-str2.length())
.forEach(str -> System.out.println(str));
5. map()
把所有元素經過處理後返回。抽象函式為:
<R> Stream<R> map(Function<? super T,? extends R> mapper)
用法為:
Stream<String> stream = Stream.of("I", "love", "you", "too");
stream.map(str -> str.toUpperCase())
.forEach(str -> System.out.println(str));
6. flatMap()
把多個集合物件拆開變成一個stream。使用樣例:
Stream<List<Integer>> stream = Stream.of(Arrays.asList(1,2), Arrays.asList(3, 4, 5));
stream.flatMap(list -> list.stream())
.forEach(i -> System.out.println(i));
Stream的reduce操作
reduce操作就是從眾多元素中篩選出一個你想要的值。sum()、max()、min()、count()等都是reduce操作,將他們單獨設為函式只是因為常用。
reduce在Stream裡面有三種過載的方法分別是:
Optional<T> reduce(BinaryOperator<T> accumulator);
T reduce(T identity, BinaryOperator<T> accumulator);
<U> U reduce(U identity,
BiFunction<U, ? super T, U> accumulator,
BinaryOperator<U> combiner);
下面講一下這三個方法有什麼區別。
第一個方法應用如下:
List<Integer> numList = Arrays.asList(1,2,3,4,5);
int result = numList.stream().reduce((a,b) -> a + b ).get();
System.out.println(result);
這段程式碼最後返回的就是元素的累加。
那麼第二個過載方法和第一基本一樣,區別就是在執行的時候第一個元素並不是list裡面的元素而是你放入的元素。
List<Integer> list = new ArrayList<>(Arrays.asList(1,2,3));
int x = list.stream().reduce(5,(a,b)->{
System.out.println(a+","+b);
return a+b;
});
System.out.println(x);
這個程式碼的輸出是:
5,1
6,2
8,3
11
可以看到第一個元素是5。最後的結果也是11。
第三個方法作用特別大,就是可以進行型別轉換。程式碼如下:
List<Integer> list = new ArrayList<>(Arrays.asList(1,2,3));
Long x = list.stream().reduce(3L,(a, b)->{
System.out.println(a+","+b);
return a+b;
},(a,b)->null);
System.out.println(x);
最後把int值轉換成了long型別。最後一個元素用於並行的stream。一般用不到。
Stream裡面的collect()
collect的作用就是把stream轉換成其他的類比如List,Set,Map。比如:
Stream<String> stream = Stream.of("I", "love", "you", "too");
List<String> list = stream.collect(ArrayList::new, ArrayList::add, ArrayList::addAll);// 方式1
//List<String> list = stream.collect(Collectors.toList());// 方式2
System.out.println(list);
我們看到裡面有用到[類名]::[方法名]。這種方式就是將類裡面的方法當作lambda方法傳入進去。
collect()方法有兩種過載方法分別是:
<R> R collect(Supplier<R> supplier,
BiConsumer<R, ? super T> accumulator,
BiConsumer<R, R> combiner);
<R, A> R collect(Collector<? super T, A, R> collector);
上面的樣例程式碼就是用的第一種,我們需要提供三個引數。但是如果我們轉換的是常用類的話,Collector這個類已經幫我們實現好了。所以我們用第二個過載方法會簡單很多。例如:
Stream<String> stream = Stream.of("I", "love", "you", "too");
List<String> list = stream.collect(Collectors.toList()); // (1)
Set<String> set = stream.collect(Collectors.toSet()); // (2)
上述程式碼能夠滿足大部分需求,但由於返回結果是介面型別,我們並不知道類庫實際選擇的容器型別是什麼,有時候我們可能會想要人為指定容器的實際型別,這個需求可通過Collectors.toCollection(Supplier collectionFactory)方法完成。
ArrayList<String> arrayList = stream.collect(Collectors.toCollection(ArrayList::new));// (3)
HashSet<String> hashSet = stream.collect(Collectors.toCollection(HashSet::new));// (4)
collect()如何生成Map
雖然Map不能轉換成stream,但是反過來是可以的。有三種方法可以生成map:
- 使用Collectors.toMap()生成的收集器,使用者需要指定如何生成Map的key和value。
- 使用Collectors.partitioningBy()生成的收集器,對元素進行二分割槽操作時用到。
- 使用Collectors.groupingBy()生成的收集器,對元素做group操作時用到。
下面分別講述這三個方法都是什麼意思
首先第一個方法為:基於元素可以傳入兩個lambda方法,之後這兩個lambda方法的返回值就是key與value。
List<Integer> list = new ArrayList<>(Arrays.asList(1,2,3));
Map<Integer, String> map = list.stream().collect(Collectors.toMap(a->a+1,a->a.toString()));
System.out.println(map);
輸出為:
{2=1, 3=2, 4=3}
第二個方法就是將元素分為兩個部分,根據就是第二個傳入的lambda函式。我們可以把它看作一個條件。滿足條件就放到第一個集合,不滿足就是第二個。
比如:
Map<Boolean, List<Student>> passingFailing = students.stream()
.collect(Collectors.partitioningBy(s -> s.getGrade() >= PASS_THRESHOLD));
第三個方法其實課sql裡面的groupby一樣。根據一個lambda函式返回的值不同,進行不同的分類。
程式碼如下:
Map<Department, List<Employee>> byDept = employees.stream()
.collect(Collectors.groupingBy(Employee::getDepartment));
collect()還可以幫我們進行字串的拼接:
Stream<String> stream = Stream.of("I", "love", "you");
String joined = stream.collect(Collectors.joining());// "Iloveyou"
String joined = stream.collect(Collectors.joining(","));// "I,love,you"
String joined = stream.collect(Collectors.joining(",", "{", "}"));// "{I,love,you}"
Lambda表示式的用法介紹就全部結束了,但是他的內部原理還要更加的複雜。這裡涉及到對於Stream這個介面的實現,也就是ReferencePipeline這個類。他對Stream有一定的優化作用。具體的你們可以看我在文章開頭附上的那片文章,他在後邊有講到這些。如果有時間我也會在以後的文章繼續講解這些知識。謝謝大家~
相關文章
- 掌握 Java 8 Lambda 表示式Java
- Java中Lambda表示式的應用Java
- Lambda 表示式的應用
- Java | Lambda表示式Java
- Lambda表示式(Java)Java
- java lambda 表示式Java
- Java Lambda表示式Java
- [轉]Java 8 的 lambda 表示式 Java 8 的 lambda 表示式Java
- lambda表示式——快速入門
- Java之lambda表示式Java
- Java的Lambda表示式Java
- Java 8 Lambda 表示式Java
- java 8 lambda表示式Java
- Java Lambda 表示式初探Java
- Java 8:一文帶你掌握 Lambda 表示式Java
- Java 基礎 —— Lambda 表示式Java
- Java 中的 Lambda 表示式Java
- Java lambda表示式基本使用Java
- Java8-Lambda表示式Java
- java8 lambda表示式Java
- Java筆記:Lambda表示式Java筆記
- lambda 表示式
- lambda表示式
- Lambda表示式基本語法與應用
- Java8的Lambda表示式Java
- Java中lambda表示式詳解Java
- Java中Lambda表示式的使用Java
- Java 8 中的 lambda 表示式Java
- 深入探索 Java 8 Lambda 表示式Java
- java8特性-lambda表示式Java
- JDK1.8中Lambda表示式的應用JDK
- 八,Lambda表示式
- 《Java 8 in Action》Chapter 3:Lambda表示式JavaAPT
- Java 8新特性(一):Lambda表示式Java
- Java8中的Lambda表示式Java
- Java入門:Lambda常用表示式解析Java
- Java 8 lambda 表示式10個示例Java
- Java Lambda 表示式學習筆記Java筆記