《Java 8 函數式編程》的筆記
簡單mark下里面的代碼
習題解:https://github.com/RichardWarburton/java-8-lambdas-exercises
2.Lamba表達式
相當於匿名方法,代碼即數據,閉包且適用於函數接口。
Lamba可應用在
匿名內部類 button.addActionListenr( event -> System.out.println("button clicked") )
javac(編譯器)會根據程序上下文推斷出參數類型(有時不一定),類型推斷始於Java7,`new HashMap<>
中的<>就會推斷泛型參數的類型
局部變量BinaryOperator<Long> add = (x, y) -> x+y;
,這樣add相當於x+y
這行代碼,而目標類型爲Long
(上下文環境類型,就像數組對象和null賦值後才知道類型)。
引用所在方法變量時,聲明爲final,實際引用的是該值非變量。如
final String name = getUserName();
button.addActionListener( event -> System.out.println("hi"+name) );
這時final在Java8中可省。
函數接口有
Predicate<T> test
Consumer<T> accept
Function<T,R> apply
Supplier<T> get
UnaryOperator<T>
BinaryOperator<T>
3.Stream
Stream爲內部迭代(集合類中實現,無需在程序中編寫),如stream()返回對象,而非iterator()返回用於外部迭代的Iterator對象
int count = 0;
for (Artist artist : allArtists)
if (artist.isFrom("London"))
count++;
//前者爲封裝了迭代的語法糖
int count = 0;
Iterator<Artist> iterator = allArtists.iterator();
while (iterator.hasNext()){
Artist artist = iterator.next();
if (artist.isFrom("London"))
count++;
}
鏈式調用由多個Lazy操作組成,以Eager操作結束,只迭代一次。
allArtists.stream()
.filter( artist -> artist.isFrom("London") )
.count();
常用的操作
//collect
List<String> collected = Stream.of("a", "b", "c")
.collect( Collectors.toList() );
assertEquals( Arrays.asList("a", "b", "c"), collected );
//map( Function<T, R> )
List<String> collected = Stream.of("a", "b", "c")
.map( string -> string.toUpperCase() )
.collect( Collectors.toList() );
//filter( Predicate<T, boolean> )
List<String> collected = Stream.of("a", "bb", "c")
.filter( string -> string.length >1 )
.collect( Collectors.toList() );
//flatMap( Function<T, Stream> )
List<Integer> together = Stream.of( Arrays.asList(1, 2), Arrays.asList(1, 2) )
.flatMap(numbers -> numbers.stream())
.collect( Collectors.toList() );
artists.stream()
.flatMap(artist -> Stream.of(artist.getName(), artist.getNationality()))
.collect(toList());
//max.min
Artist artist = allArtist.stream()
.min( Comparator.comparing(artist -> artist.getName().length ) )
.get();
reduce,從一組值生成一個值的操作(如count,min,max)。
int count = Stream.of(1, 2, 3)
.reduce(0, (acc, element) -> acc + element );
//展開後
BinaryOperator<Integer> accumulator = (acc, element) -> acc + element;
int count = accumulator.apply(
accumulator.apply(
accumulator.apply(0, 1),
2),
3);
//重構前
for (Album album : albums){
for (Track track : album.getTrackList()){
if (track.getLength() > 60){
String name = track.getName();
trackNames.add(name);
}
}
}
//重構後
albums.stream()
.flatMap( album -> album.getTracks() )
.filter( track -> track.getLength() > 60)
.map( track -> track.getName() )
.collect( toSet() )
儘量避免副作用,即不改變程序或外界的狀態。
4.類庫
Java泛型是基於對泛型參數類型的擦除(Object實例),List實際上得到的List,整型元素(4B)變爲指向整型對象(16B)內存的指針。
Stream對此區分(int,long,double),減少性能開銷。
public static void printTrackLengthStatistics(Album album){
IntSummaryStatistics trackLengthStats
= album.getTracks()
.mapToInt(track -> track.getLength()) //返回IntStream
.summaryStatitics();
System.out.println("Max: %d, Min: %d, Ave: %d, Sum: %d",
trackLengthStats.getMax(),
trackLengthStats.getMin(),
trackLengthStats.getAverage(),
trackLengthStats.getSum())
}
重載解析時,lamba作爲參數時,類型由目標類型推到得出。從函數接口參數或最具體的類型推導出。
函數接口需要添加@FunctionalInterface
爲了二進制接口的兼容性(Collection接口添加了stream方法),Java8添加默認方法。
//Iterable
default void foreach(Consumer< ? super T> action){
for (T t : this){
action.accept(t);
}
}
與靜態方法相反,與類定義的方法產生衝突時,優先選擇類的具體方法。
因此產生的多重繼承(避免對象狀態的繼承),需要在子類中Override
public class MusicalCarriage implements Carriage, Jukebox{
@Override
public String rock(){
return Carriage.super.rock();
}
}
同時添加了接口的靜態方法特性,如Stream.of。
爲了避免引用null值的變量,可以使用Optional。
Optional<Stirng> a = Optional.of("a");
Optional emptyOptional = Optional.empty();
Optional emptyOptional = Optional.ofNullable(null);
assertEquals(a.isPresent());
assertEquals("b", emptyOptional.ofElse("b"));
assertEquals("c", emptyOptional.ofElseGet( ()->"c" ));
5.高級集合類和收集器
方法引用,即ClassName::methodName,如Artist::new、String[]::new。
流中元素的出現順序與創建的集合有關,有序較友好,按序處理用forEachOrdered。
轉換爲其他集合、值
stream.collect(toCollection(TreeSet::new));
//找出最大
public Optional<Artist> biggestGroup(Stream<Artist> artists){
Function<Artist, Long> getCount = artist -> artist.getMemebers().count();
artists.collect(maxBy(comparing(getCount)));
}
//找出平均數
public double averageNumberOfTracks(List<Album> albums){
return albums.stream().
.collect(averageingInt(album -> album.getTrackList().sizeA() ));
}
數據分塊
public Map<Boolean, List<Artist>> bandAndSolo(Stream<Artist> artists){
return artists.collect(partitioningBy(artist -> artist.isSolo()));
}
數據分組
public Map<Boolean, List<Artist>> bandAndSolo(Stream<Artist> artists){
return artists.collect(groupingBy(album -> album.getMainMusician()));
}
字符串
StringBuilder builder = new StringBuilder("[");
for(Artist artist : artists){
if (builder.length()>1)
builder.append(", ");
String name = artist.getName();
builder.append(name);
}
builder.append("]");
String result = builder.toString();
//Stream
String result = artists.stream()
.map( artist -> artist.geName() )
.collect( Collectors.joining(", ", "[", "]") );
組合收集器
Map<Artist, Long> a = albums.collect(groupingBy(album -> album.getMainMusician(), counting()));
Map<Artist, List<String>> b = albums.collect(groupingBy(Album::getMainMusician, mapping(Album::getName, toList())));
重構收集器
StringCombiner combined = artists.stream()
.map(Artist::getName)
.reduce(new StringCombiner(", ", "[", "]"),
StringCombiner::add, StringCombiner::merage);
String result = combined.toString();
//定製化
StringCombiner combined = artists.stream()
.map(Artist::getName)
.collect(Collectors.reducing(new StringCombiner(", ", "[", "]"),
name -> new StringCombiner(", ", "[", "]").add(name), StringCombiner::merge));
一些細節
//讀緩存
public Artist getArtist(String name){
Artist artist = artistCache.get(name);
if(artist == null){
artist = readArtistFromDB(name);
artistCache.put(name);
}
return artist;
}
//Stream
public Artist getArtist(String name){
return artistCache.computeIfAbsent(name, this::readArtistFromDB);
}
//在Map上迭代
Map<Artist, Integer> countOfAlbums = new HashMap<>();
for(Map.Entry<Artist, List<Album>> entry:albumsByArtist.entrySet()){
Artist artist = entry.getKey();
List<Album> albums = entry.getValue();
aountOfAlbums.put(artist, albums.size());
}
//Stream
Map<Artist, Integer> countOfAlbums = new HashMap<>();
albumsByArtist.forEach( (artist, albums) -> {aountOfAlbums.put(artist, albums.size());} );
6.數據並行化
併發爲共享時間,並行爲同時發生,有數據並行化和任務並行化,利用多核。
parallel和parallelStream
//蒙特卡洛模擬法並行化模擬擲骰子事件
public Map<Integer, Double> parallelDiceRolls() {
double fraction = 1.0/N;
return IntStream.range(0, N)
.parallel()
.mapToObj(twoDiceThrows())
.collect(groupingBy(side -> side, summingDouble(n -> fraction)));
}
//手動模擬
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ThreadLocalRandom;
public class ManualDiceRolls {
private static final int N = 1000;
private final double fraction;
private final Map<Integer, Double> results;
private final int numberOfThreads;
private final ExecutorService executor;
private final int workPerThread;
public static void main(String[] args){
ManualDiceRolls roles = new ManualDiceRolls();
roles.simulateDiceRoles();
}
public ManualDiceRolls(){
fraction = 1.0/N;
results = new ConcurrentHashMap<>();
numberOfThreads = Runtime.getRuntime().availableProcessors();
executor = Executors.newFixedThreadPool(numberOfThreads);
workPerThread = N / numberOfThreads;
}
public void simulateDiceRoles(){
List<Future<?>> futures = submitJobs();
awaitCompletion(futures);
printResults();
}
private void printResults(){
results.entrySet().forEach(System.out::println);
}
private List<Future<?>> submitJobs(){
List<Future<?>> futures = new ArrayList<>();
for ( int i=0; i<numberOfThreads; i++){
futures.add(executor.submit(makeJob()));
}
return futures;
}
private Runnable makeJob(){
return () -> {
ThreadLocalRandom random = ThreadLocalRandom.current();
for ( int i=0; i<workPerThread; i++){
int entry = twoDiceThrows(random);
accumlateResult(entry);
}
};
}
private void accumlateResult(int entry){
results.compute( entry,
(key, previous) -> previous==null ?fraction :previous+fraction
);
}
private int twoDiceThrows(ThreadLocalRandom random){
int firstThrow = random.nextInt(1, 7);
int secondThrow = random.nextInt(1, 7);
return firstThrow+secondThrow;
}
private void awaitCompletion(List<Future<?>> futures){
futures.forEach( (future) -> {
try {
future.get();
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
);
executor.shutdown();
}
}
限制
reduce操作在並行時,初值爲組合函數的恆等值,必須符合結合律。
性能
主要由數據大小,源數據結構,裝箱,核的數量,單元處理開銷。
按源數據結構(分解方式:對半),性能好:ArrrayList、IntStream.range等;性能一般:HashSet、TreeSet等;性能差:LinkedList、Streams.iterate、BufferedRead.lines。
無狀態並行操作,性能更好,如map、filter、faltMap,反之sorted、distinct、limit。
private int addIntegers(List<Integer> values){
return values.parallelStream()
.mapToInt( i -> i )
.sum();
}
並行化數組操作
//for循環
public static double[] imperativeInitilize(int size){
double[] values = new double[size];
for (int i=0; i<values.length; i++){
values[i] = i;
}
return values;
}
//並行化初始
public static double[] parallelInitialize(int size){
double[] values = new double[size];
Arrays.parallelSetAll(values, i->i);
return values;
}
//對時間序列計算簡單滑動平均數
public static double[] simpleMovingAverage(double[] values, int n){
double[] sums = Arrays.copyOf(values, values.length);
Arrays.parallelPrefix(sums, Double::sum);
int start = n-1;
return IntStream.range(start, sums.length)
.mapToDouble(i -> {
double prefix = i==start ? 0: sums[i-n];
return (sums[i]-prefix)/n;
})
.toArray();
}
7.測試、調試、重構
//if(logger.isDebugEnabled())
Logger logger = new Loggger();
logger.debug(()->"Look at"+expensiveOperation());
//匿名類
ThreadLocal<Album> thisAlbum = ThreadLocal.withInitial( ()->database.lookupCurrentAlbum() );
//Write Everything Twice
public long countFeature(ToLongFunction<Album> function){
return albums.stream()
.mapToLong(function)
.sum();
}
public logn countTracks(){
return countFeature( album -> album.getTracks().count());
}
public long countRunningTime(){
return countFeature(album -> album.getTracks()
.mapToLong( track -> track.getLength() )
.sum());
}
單元測試
使用方法引用
public static List<String> elementFirstToUppercase(List<String> words){
return words.stream()
.map(Testing::firstToUppercase)
.collect(Collectors.<String>toList());
}
public static String firstToUppercase(Stirng value){
char firstChar = Character.toUpperCase(value.charAt(0));
return firstChar+value.substring(1);
}
@Test
public void twoLetterStringConvertedToUpperCase(){
String input = "ab";
String result = Testing.firstToUpperCase(input);
assertEquals("Ab", result);
}
@Test
public void canCountFeatures(){
OrderDomain order = new OrderDomain( asList(
newAlbum("A"),newAlbum("B"),newAlbum("C"),newAlbum("D")
) );
assertEquals(8, order.countFeature(album->2));
}
//結合Mockito框架
List<String> list = mock(List.class);
when (list.size()).thenAnswer(inv->otherList.size());
assertEquals(3, list.size());
使用peek,記錄中間值。查看流中的每個值並操作,調試
Set<String> nationalities = album.getMusicians()
.filter(artist -> artist.getName().startsWith("The"))
.map(artist -> artist.getNationality())
.peek(nation -> System.out.println("Nation:"+nation))
.collect(Collectors.<String>toSet());
8.設計和架構的原則
lambda改變設計模式
命令行模式
public interface Editor{
public void open();
public void save();
public void close();
}
public interface Action{
public void perform();
}
public class Open implements Action{
private final Editor editor;
public Open(Editor editor){
this.editor = editor;
}
@Override
public void perform(){
editor.open();
}
}
publvi class Macro {
private final List<Action> actions;
public Macro(){
actions = new ArrayList<Action>;
}
public void record(Action action){
actions.add(action);
}
public void run(){
acitons.forEach(Action::perform);
}
}
Macro macro = new Macro();
macro.record(new Open(editor));
macro.record(new Save(editor));
macro.record(new Close(editor));
marco.run();
//lambda
Macro macro = new Macro();
macro.record(editor::open);
macro.record(editor::save);
macro.record(editor::close);
marco.run();
策略模式
public class Compressor{
private final CompressionStrategy strategy;
public Compressor(CompressionStrategy strategy){
this.strategy = strategy;
}
public void compress(Path inFile, File outFile) throws IOException {
try(OutputStream outStream = new FileOutputStream(outFile)){
File.copy(inFile, strategy.compress(outStream));
}
}
}
public class GzipCompressionStrategy implements CompressionStrategy{
@Override
public OutputStream compress(OutputStream data) throws IOException{
return new GZIPOutputStream(data);
}
}
Compress gzipCompressor = new Compress(new GzipCompressionStrategy);
gzipCompressor.compress(inFile, outFile);
Compress zipCompressor = new Compress(new ZipCompressionStrategy);
zipCompressor.compress(inFile, outFile);
//lambda
Compress gzipCompressor = new Compress(GZIPOutputStream::new);
gzipCompressor.compress(inFile, outFile);
Compress zipCompressor = new Compress(ZIPOutputStream::new);
zipCompressor.compress(inFile, outFile);
觀察者模式
public class Nasa implements LandingObserver{
@Override
public void observeLanding(String name){
if(name.contains("Apollo")){
System.out.println("We made it~");
}
}
}
public class Moon{
private final List<LandingObserver> observers = new ArrayList<>();
public void land(String name){
for (LandingObserver observer: observers){
observer.observeLanding(name);
}
}
public void startSpying(LandingObserver observer) {
observers.add(observer);
}
}
Moon moon = new Moon();
moon.startSpying(new Nasa());
moon.startSpying(new Aliens());
moon.land("An asteroid");moon.land("Apollo 11");
//lambda
Moon moon = new Moon();
moon.startSpying(name -> {
if(name.contains("Apollo")){
System.out.println("We made it~");
}
});
moon.startSpying(name -> {
if(name.contains("Apollo")){
System.out.println("They're distracted.");
}
});
moon.startSpying(new Aliens());
moon.land("An asteroid");moon.land("Apollo 11");
模板方法模式
public abstract class LoanApplication{
public void checkLoanApplication() throws ApplicationDenied{
checkIdentity();
checkCreditHistory();
checkIncomeHistroy();
reportFindings();
}
public void checkLoanApplication() throws ApplicationDenied;
public void checkCreditHistory() throws ApplicationDenied;
public void checkIncomeHistroy() throws ApplicationDenied;
private void reportFindings(){ ... }
}
//lamba
public class LoanApplication{
private final Criteria identity;
private final Criteria creditHistory;
private final Criteria incomeHistroy;
public void LoanApplication(Criteria identity,Criteria creditHistory,Criteria incomeHistroy){
this.identity = identity;
this.creditHistory = creditHistory;
this.incomeHistroy = incomeHistroy;
}
public void checkLoanApplication() throws ApplicationDenied{
identity.check();
creditHistory.check();
incomeHistroy.check();
reportFindings();
}
private void reportFindings(){ ... }
}
public interface Criteria{
publci void check() throws ApplicationDenied;
}
public CompanyLoanApplication extends LoanApplication{
public CompanyLoanApplication(Company company){
super(company::checkHistory,company::checkHistoricalDebt,company::checkProfitAndLoss);
}
}
使用lambda表達式的DSL
流暢性依賴於善用IDE的自動補全
public static void describe(String name, Suite behavior){
Description description = new Description(name);
behavior.specifySuite(description);
}
public void should(String description, Specification specification){
try{
Except except = new Except();
specification.specifyBehaviour(except);
Runner.current.recordSuccess(suite, description);
}catch(AssertionError cause){
Runner.current.recordFailure(suite, description, cause);
}catch(Throwable cause){
Runner.current.recordError(suite, description, cause);
}
}
//except.that(stack.pop()).isEqualTo(2);
public final class Except{
public BoundException that(Object value){
return new BoundException(value);
}
}
public class StackSepc{{
...
}}
//==
public class StackSepc{
public StackSepc(){
...
}
}
使用lambda表達式的SOLID
單一功能原則
public long countPrimes(int upTo){
long tally = 0;
for (int i=0; i<upTo; i++){
boolean isPrime = true;
for (int j=2; j<i; i++){
if(i%j==0){
isPrime = false;
}
}
if(isPrime){
tally++;
}
}
return tally;
}
//重構
public long countPrimes(int upTo){
long tally = 0;
for (int i=0; i<upTo; i++){
boolean isPrime = true;
if(isPrime(i)){
tally++;
}
}
return tally;
}
private boolean isPrime(int number){
for (int j=2; j<number; i++){
if(i%j==0){
isPrime = false;
}
}
}
//集合流重構
public long countPrimes(int upTo){
return IntStream.range(1, upTo)
//.parallel() //並行
.filter(this::Prime)
.count();
}
public long isPrime(int number){
return IntStream.range(2, number)
.allMatch(x -> (number%x) != 0);
}
開閉原則
如不可變對象String,首次調用hashCode方法緩存了生成的哈希值
ThreadLocal<DateFormat> localFormatter = ThreadLocal.withInitial(()->new SimpleDateFormat());
DateFormat formatter = localFormatter.get();
AtomicInteger threadId = new AtomicInteger();
ThreadLocal<Integer> localId = ThreadLocal.withInitial(()->threadId.getAndIncrement());
int idForThisThread = localId.get();
依賴反轉原則
細節反而以來抽象(如上層邏輯依賴底層抽象)
public List<String> findHeadings(Reader input){
try(BufferedReader reader = new BufferedReader(input)){
return reader.lines()
.filter(line -> line.endsWith(":"))
.map(line -> line.substring(0, line.length()-1))
.collect(toList());
}catch(IOException e){
throw new HadingLookuoException(e);
}
}
//重構
public List<String> findHeadings(Reader input){
return withLinesOf(reader.lines(), lines -> lines.filter(line -> line.endsWith(":"))
.map(line -> line.substring(0, line.length()-1))
.collect(toList(), HadingLookuoException::new));
}
public <T> T withLinesOf(Reader input, Function<Stream<String>> handler, Function<IoException, RuntimeException error){
try(BufferedReader reader = new BufferedReader(input)){
return handler.apply(reader.lines());
}catch(IOException e){
throw error.apply(e);
}
}
9.使用lambda表達式編寫併發程序
使用Vert.x和RxJava框架,非阻塞式IO
回調
//Vert.x下聊天應用
//接受TCP連接
public class ChatVerticle extends Verticle{
public void start(){
vertx.createNetServer()
.connectHandler(socket -> { //回調
container.logger().info("socket connected");
socket.dataHandler(new User(socket, this));
}
)
.listen(10_000);
}
}
//處理用戶連接
public class User implements Handler<Buffer>{
private static final Pattern newline = Pattern.compile("\\n");
private final NetSocket socket;
private final Set<String> names;
private final EventBus eventBus;
private Optional<String> name;
public User(NetSocket socket, Verticle verticle){
Vertx vertx = verticle.getVertx();
this.socket = socket;
names = vertx.sharedData().getSet("names");
eventBus = vertx.eventBus();
name = Optional.empty();
}
@Override
public void handler(Buffer buffer){
newline.splitAsStream(buffer.toString())
.forEach(line -> {
if(!name.isPresent())
setName(line);
else
handlerMessage(line);
});
....
}
//註冊聊天消息
eventBus.registerHandler(name, (Message<String> msg) -> {
sendClient(msg.body);
} );
//發送聊天信息
eventBus.send(user, name.get()+">"+message);
//羣發消息
private void broadcastMessage(String message){
String name = this.name.get();
eventBus.publish(name+".followers", name+">"+message);
}
//接受羣發消息
private void followUser(String user){
eventBus.registerHandler(user+".followers", (Message<String> msg) -> {
sendClient(msg.body);
} );
}
消息傳遞架構
Vert.x通過複製發送的消息,避免共享狀態。易於測試,隔離錯誤,不必重啓JVM,重啓本地Verticle對象即可。
@Test //聊天程序服務端
public void canMessageFriend(){
withModule(this::messageFriendWithModule);
}
private void messageFriendWithModule(){
withConection(richard -> {
checkBobReplies(richard);
richard.write("richard\n");
messageBob(richard);
});
}
private void checkBobReplies(NetSocket richard){
richard.handler(data -> {
assertEquals("bob>oh its you!", data.toString());
moduleTestComplete();
});
}
private void messageBob(NetSocket richard){
withConection(messageBobWithConnection(richard));
}
private Handler<NetSocket> messageBobWithConnection(NetSocket bob){
return bob -> {
checkRichardMessageYou(bob);
bob.write("bob\n");
vertx.setTimer(6, id -> richard.write("bob<hai"));
};
}
private void checkRichardMessageYou(NetSocket bob){
bob.handler(data -> {
assertEquals("richard>hai", data.toString());
bob.write("richard<oh its you!");
});
}
使用Future並行操作
public Album lookupByName(String albumName){
FUture<Credentials> trackLogin = loginTo("track");
FUture<Credentials> artistLogin = loginTo("artist");
try{//上兩者阻塞
Future<List<Track>> tracks = lookupTracks(albumName, trackLogin.get());
Future<List<Artist>> aritists = lookupTracks(albumName, artistLogin.get());
return new Album(albunName, tracks.get(), artitsts.get());
}catch(InterruptedException | ExecutionException e){
throw new AlbumLookupException(e.getCause());
}
}
//使用CompletableFuture
public Album lookupByName(String albumName){
CompletaleFuture<List<Artist>> artistLookup = loginTo("artist")
.thenCompose(artistLogin -> lookupArtists(album, artistLogin));
return loginTo("track")
.thenCompose(trackLogin -> lookupTracks(album, trackLogin))
.thenCombine(artistLookup, (tracks, artists) -> new Album(albunName, tracks, artitsts) )
.join();
}
//創建CompleteableFuture
CompleteableFuture<Artist> createFuture(String id){
CompleteableFuture<Artist> future = new CompleteableFuture<>();
startJob(future);//在另外線程模型中後續賦值
return future;
}
//提供值
future.complete(artist);
//異步執行
CompleteableFuture<Track> lookupTrack(String id){
return CompleteableFuture.supplyAsync(()->{
...
return track;
}, service);
}
//出錯時
future.completeExceptionally(AlbumLookupException("Unable to find "+name));
響應式編程
//RxJava 組合異步和基於事件的系統流程,處理數據流
public Observable<Artist> search(String searchedName, String searchedNationality, int maxResults){
return getSavedArtists()
.filter(name -> name.contains(searchedName))
.flatMap(this::lookupArtist)
.filter(artist -> artist.getNationality().contains(searchedNationality))
.tack(maxResults);
}
//傳值
observer.onNext("a");
observer.onCompleted();
//Error
observer.onError(new Exception());