Lọc Java Stream thành 1 và chỉ 1 phần tử


230

Tôi đang cố gắng sử dụng Java 8 Streamđể tìm các phần tử trong a LinkedList. Tôi muốn đảm bảo, tuy nhiên, có một và chỉ một phù hợp với tiêu chí lọc.

Lấy mã này:

public static void main(String[] args) {

    LinkedList<User> users = new LinkedList<>();
    users.add(new User(1, "User1"));
    users.add(new User(2, "User2"));
    users.add(new User(3, "User3"));

    User match = users.stream().filter((user) -> user.getId() == 1).findAny().get();
    System.out.println(match.toString());
}

static class User {

    @Override
    public String toString() {
        return id + " - " + username;
    }

    int id;
    String username;

    public User() {
    }

    public User(int id, String username) {
        this.id = id;
        this.username = username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getUsername() {
        return username;
    }

    public int getId() {
        return id;
    }
}

Mã này tìm thấy Userdựa trên ID của họ. Nhưng không có gì đảm bảo có bao nhiêu Users khớp với bộ lọc.

Thay đổi dòng bộ lọc thành:

User match = users.stream().filter((user) -> user.getId() < 0).findAny().get();

Sẽ ném một NoSuchElementException(tốt!)

Tôi muốn nó ném một lỗi nếu có nhiều trận đấu, mặc dù. Có cách nào để làm việc này không?


count()là một hoạt động thiết bị đầu cuối vì vậy bạn không thể làm điều đó. Luồng không thể được sử dụng sau.
Alexis C.

Ok, cảm ơn @ZouZou. Tôi không hoàn toàn chắc chắn những gì phương pháp đó đã làm. Tại sao không có Stream::size?
ryaugeage

7
@ryaugeage Vì một luồng chỉ có thể được sử dụng một lần: tính toán kích thước của nó có nghĩa là "lặp lại" trên luồng đó và sau đó bạn không thể sử dụng luồng đó nữa.
assylias

3
Ồ Một bình luận đó đã giúp tôi hiểu được Streamnhiều hơn những gì tôi đã làm trước đây ...
ryaugeage

2
Đây là khi bạn nhận ra rằng bạn cần phải sử dụng một LinkedHashSet(giả sử bạn muốn giữ trật tự chèn) hoặc HashSettất cả cùng. Nếu bộ sưu tập của bạn chỉ được sử dụng để tìm một id người dùng, vậy tại sao bạn lại thu thập tất cả các mục khác? Nếu có một tiềm năng mà bạn sẽ luôn cần tìm một số id người dùng cũng cần phải là duy nhất, thì tại sao lại sử dụng danh sách mà không phải là một tập hợp? Bạn đang lập trình ngược. Sử dụng bộ sưu tập phù hợp cho công việc và tự cứu mình khỏi cơn đau đầu này
smac89

Câu trả lời:


192

Tạo một tùy chỉnh Collector

public static <T> Collector<T, ?, T> toSingleton() {
    return Collectors.collectingAndThen(
            Collectors.toList(),
            list -> {
                if (list.size() != 1) {
                    throw new IllegalStateException();
                }
                return list.get(0);
            }
    );
}

Chúng tôi sử dụng Collectors.collectingAndThenđể xây dựng mong muốn của chúng tôi Collectorbằng cách

  1. Thu thập các đối tượng của chúng tôi trong một Listvới các Collectors.toList()nhà sưu tập.
  2. Áp dụng một bộ hoàn thiện bổ sung vào cuối, trả về phần tử đơn - hoặc ném IllegalStateExceptionif list.size != 1.

Được dùng như:

User resultUser = users.stream()
        .filter(user -> user.getId() > 0)
        .collect(toSingleton());

Sau đó, bạn có thể tùy chỉnh điều này Collectornhiều như bạn muốn, ví dụ đưa ra ngoại lệ làm đối số trong hàm tạo, điều chỉnh nó để cho phép hai giá trị và hơn thế nữa.

Một giải pháp thay thế - được cho là kém thanh lịch -:

Bạn có thể sử dụng một 'cách giải quyết' liên quan peek()và một AtomicInteger, nhưng thực sự bạn không nên sử dụng nó.

Những gì bạn có thể làm istead chỉ là thu thập nó trong một List, như thế này:

LinkedList<User> users = new LinkedList<>();
users.add(new User(1, "User1"));
users.add(new User(2, "User2"));
users.add(new User(3, "User3"));
List<User> resultUserList = users.stream()
        .filter(user -> user.getId() == 1)
        .collect(Collectors.toList());
if (resultUserList.size() != 1) {
    throw new IllegalStateException();
}
User resultUser = resultUserList.get(0);

24
Quả ổi Iterables.getOnlyElementsẽ rút ngắn các giải pháp này và cung cấp các thông báo lỗi tốt hơn. Chỉ là một mẹo cho những độc giả đã sử dụng Google Guava.
Tim Büthe

2
tôi gói ý tưởng này thành một lớp - gist.github.com/denov/a7eac36a3cda041f8afispcef09d16fc
denov

1
@LonelyNeuron Vui lòng không chỉnh sửa mã của tôi. Nó đặt tôi vào một tình huống mà tôi cần xác thực toàn bộ câu trả lời của mình, điều mà tôi đã viết bốn năm trước, và đơn giản là tôi không có thời gian cho nó ngay bây giờ.
skiwi

2
@skiwi: Chỉnh sửa của Lonely rất hữu ích và chính xác, vì vậy tôi đã giới thiệu lại nó sau khi xem xét. Những người truy cập câu trả lời này hôm nay không quan tâm đến cách bạn đi đến câu trả lời, họ không cần phải xem phiên bản cũ và phiên bản mới và phần Cập nhật . Điều đó làm cho câu trả lời của bạn khó hiểu hơn và ít hữu ích hơn. Sẽ tốt hơn nhiều nếu đặt bài viết ở trạng thái cuối cùng , và nếu mọi người muốn xem tất cả diễn ra như thế nào, họ có thể xem lịch sử bài đăng.
Martijn Pieters

1
@skiwi: Mã trong câu trả lời hoàn toàn là những gì bạn đã viết. Tất cả các trình soạn thảo đã làm sạch bài đăng của bạn, chỉ xóa một phiên bản trước đó của singletonCollector()định nghĩa bị lỗi thời bởi phiên bản còn lại trong bài đăng và đổi tên thành toSingleton(). Chuyên môn về luồng Java của tôi hơi thô lỗ, nhưng việc đổi tên có vẻ hữu ích với tôi. Xem lại thay đổi này mất tôi 2 phút, ngọn. Nếu bạn không có thời gian để xem xét các chỉnh sửa, tôi có thể đề nghị bạn yêu cầu người khác thực hiện việc này trong tương lai, có lẽ trong phòng trò chuyện Java không?
Martijn Pieters

118

Để hoàn thiện, đây là 'một lớp lót' tương ứng với câu trả lời xuất sắc của @ prunge:

User user1 = users.stream()
        .filter(user -> user.getId() == 1)
        .reduce((a, b) -> {
            throw new IllegalStateException("Multiple elements: " + a + ", " + b);
        })
        .get();

Điều này có được yếu tố phù hợp duy nhất từ ​​luồng, ném

  • NoSuchElementException trong trường hợp luồng trống, hoặc
  • IllegalStateException trong trường hợp luồng chứa nhiều phần tử khớp.

Một biến thể của phương pháp này sẽ tránh đưa ra một ngoại lệ sớm và thay vào đó biểu thị kết quả là một Optionalphần tử duy nhất hoặc không có gì (trống) nếu không có hoặc có nhiều phần tử:

Optional<User> user1 = users.stream()
        .filter(user -> user.getId() == 1)
        .collect(Collectors.reducing((a, b) -> null));

3
Tôi thích cách tiếp cận ban đầu trong câu trả lời này. Đối với mục đích tùy biến, nó có thể chuyển đổi cuối cùng get()đểorElseThrow()
ARIN

1
Tôi thích sự ngắn gọn của cái này và thực tế là nó tránh tạo ra một thể hiện Danh sách không cần thiết mỗi khi nó được gọi.
LordOfThePigs

83

Các câu trả lời khác liên quan đến việc viết một tùy chỉnh Collectorcó thể hiệu quả hơn (chẳng hạn như Louis Wasserman , +1), nhưng nếu bạn muốn ngắn gọn, tôi đề nghị như sau:

List<User> result = users.stream()
    .filter(user -> user.getId() == 1)
    .limit(2)
    .collect(Collectors.toList());

Sau đó xác minh kích thước của danh sách kết quả.

if (result.size() != 1) {
  throw new IllegalStateException("Expected exactly one user but got " + result);
User user = result.get(0);
}

5
Điểm của limit(2)giải pháp này là gì? Điều gì khác biệt sẽ làm cho dù danh sách kết quả là 2 hoặc 100? Nếu nó lớn hơn 1.
ry Bổ trợ

18
Nó dừng lại ngay lập tức nếu nó tìm thấy một trận đấu thứ hai. Đây là những gì tất cả các nhà sưu tập ưa thích làm, chỉ cần sử dụng nhiều mã hơn. :-)
Stuart Marks

10
Cách thêmCollectors.collectingAndThen(toList(), l -> { if (l.size() == 1) return l.get(0); throw new RuntimeException(); })
Lukas Eder

1
Javadoc nói điều này về thông số giới hạn : maxSize: the number of elements the stream should be limited to. Vì vậy, không nên .limit(1)thay thế .limit(2)?
alexbt

5
@alexbt Tuyên bố vấn đề là để đảm bảo rằng có chính xác một yếu tố phù hợp (không hơn, không ít hơn). Sau mã của tôi, người ta có thể kiểm tra result.size()để đảm bảo mã đó bằng 1. Nếu là 2, thì có nhiều hơn một kết quả trùng khớp, do đó, đó là một lỗi. Nếu mã thay vào đó limit(1), nhiều hơn một trận đấu sẽ dẫn đến một yếu tố duy nhất, không thể phân biệt được với việc có chính xác một trận đấu. Điều này sẽ bỏ lỡ một trường hợp lỗi mà OP quan tâm.
Stuart Marks

67

Quả ổi cung cấp MoreCollectors.onlyElement()những gì đúng ở đây. Nhưng nếu bạn phải tự làm điều đó, bạn có thể tự mình Collectorlàm điều này:

<E> Collector<E, ?, Optional<E>> getOnly() {
  return Collector.of(
    AtomicReference::new,
    (ref, e) -> {
      if (!ref.compareAndSet(null, e)) {
         throw new IllegalArgumentException("Multiple values");
      }
    },
    (ref1, ref2) -> {
      if (ref1.get() == null) {
        return ref2;
      } else if (ref2.get() != null) {
        throw new IllegalArgumentException("Multiple values");
      } else {
        return ref1;
      }
    },
    ref -> Optional.ofNullable(ref.get()),
    Collector.Characteristics.UNORDERED);
}

... hoặc sử dụng Holderloại của riêng bạn thay vì AtomicReference. Bạn có thể tái sử dụng Collectorbao nhiêu tùy thích.


singletonCollector của @ skiwi nhỏ hơn và dễ theo dõi hơn thế này, đó là lý do tại sao tôi đưa cho anh ta tấm séc. Nhưng thật tốt khi thấy sự đồng thuận trong câu trả lời: một phong tục Collectorlà cách để đi.
ry Bổ trợ

1
Đủ công bằng. Tôi chủ yếu nhắm đến tốc độ, không phải sự đồng nhất.
Louis Wasserman

1
Vâng? Tại sao là của bạn nhanh hơn?
ry Bổ trợ

3
Chủ yếu là vì phân bổ một all-up Listđắt hơn một tài liệu tham khảo có thể thay đổi duy nhất.
Louis Wasserman

1
@LouisWasserman, câu cập nhật cuối cùng MoreCollectors.onlyElement()thực sự nên là đầu tiên (và có lẽ là duy nhất :))
Piotr Findeisen

46

Sử dụng ổi MoreCollectors.onlyElement()( JavaDoc ).

Nó thực hiện những gì bạn muốn và ném IllegalArgumentExceptionnếu luồng bao gồm hai hoặc nhiều phần tử và NoSuchElementExceptionnếu luồng trống.

Sử dụng:

import static com.google.common.collect.MoreCollectors.onlyElement;

User match =
    users.stream().filter((user) -> user.getId() < 0).collect(onlyElement());

2
Lưu ý cho những người dùng khác: MoreCollectorslà một phần của phiên bản chưa được phát hành (kể từ 2016-12) chưa được phát hành 21.
qerub

2
Câu trả lời này nên đi trên.
Emdadul Sawon

31

Hoạt động "thoát nở" cho phép bạn thực hiện những điều kỳ lạ không được luồng hỗ trợ khác là yêu cầu Iterator:

Iterator<T> it = users.stream().filter((user) -> user.getId() < 0).iterator();
if (!it.hasNext()) 
    throw new NoSuchElementException();
else {
    result = it.next();
    if (it.hasNext())
        throw new TooManyElementsException();
}

Quả ổi có một phương pháp tiện lợi để lấy Iteratorvà lấy phần tử duy nhất, ném nếu có 0 hoặc nhiều phần tử, có thể thay thế các dòng n-1 dưới cùng ở đây.


4
Phương pháp của Guava: Iterators.getOnlyEuity (Iterator <T> iterator).
anre

23

Cập nhật

Đề xuất đẹp trong nhận xét từ @Holger:

Optional<User> match = users.stream()
              .filter((user) -> user.getId() > 1)
              .reduce((u, v) -> { throw new IllegalStateException("More than one ID found") });

Câu trả lời gốc

Ngoại lệ được đưa ra Optional#get, nhưng nếu bạn có nhiều hơn một yếu tố sẽ không giúp ích. Bạn có thể thu thập người dùng trong một bộ sưu tập chỉ chấp nhận một mục, ví dụ:

User match = users.stream().filter((user) -> user.getId() > 1)
                  .collect(toCollection(() -> new ArrayBlockingQueue<User>(1)))
                  .poll();

Nó ném một java.lang.IllegalStateException: Queue full, nhưng điều đó cảm thấy quá hack.

Hoặc bạn có thể sử dụng giảm kết hợp với tùy chọn:

User match = Optional.ofNullable(users.stream().filter((user) -> user.getId() > 1)
                .reduce(null, (u, v) -> {
                    if (u != null && v != null)
                        throw new IllegalStateException("More than one ID found");
                    else return u == null ? v : u;
                })).get();

Việc giảm về cơ bản trả về:

  • null nếu không tìm thấy người dùng
  • người dùng nếu chỉ tìm thấy một
  • ném một ngoại lệ nếu tìm thấy nhiều hơn một

Kết quả sau đó được bọc trong một tùy chọn.

Nhưng giải pháp đơn giản nhất có lẽ là chỉ thu thập vào một bộ sưu tập, kiểm tra xem kích thước của nó là 1 và lấy phần tử duy nhất.


1
Tôi sẽ thêm một yếu tố nhận dạng ( null) để ngăn chặn sử dụng get(). Đáng buồn thay, bạn reducekhông làm việc như bạn nghĩ, hãy xem xét một yếu tố Streamnulltrong đó, có thể bạn nghĩ rằng bạn đã che đậy nó, nhưng tôi có thể [User#1, null, User#2, null, User#3], bây giờ nó sẽ không ném ngoại lệ tôi nghĩ, trừ khi tôi nhầm ở đây.
skiwi

2
@Skiwi nếu có phần tử null, bộ lọc sẽ ném NPE trước.
assylias

2
Vì bạn biết rằng luồng không thể chuyển nullsang hàm khử, việc loại bỏ đối số giá trị danh tính sẽ khiến toàn bộ giao dịch nulltrong hàm bị lỗi thời: reduce( (u,v) -> { throw new IllegalStateException("More than one ID found"); } )thực hiện công việc và thậm chí tốt hơn, nó đã trả về một Optionalyêu cầu cần thiết để gọi Optional.ofNullablevào kết quả.
Holger

15

Một cách khác là sử dụng rút gọn: (ví dụ này sử dụng chuỗi nhưng có thể dễ dàng áp dụng cho bất kỳ loại đối tượng nào bao gồm User)

List<String> list = ImmutableList.of("one", "two", "three", "four", "five", "two");
String match = list.stream().filter("two"::equals).reduce(thereCanBeOnlyOne()).get();
//throws NoSuchElementException if there are no matching elements - "zero"
//throws RuntimeException if duplicates are found - "two"
//otherwise returns the match - "one"
...

//Reduction operator that throws RuntimeException if there are duplicates
private static <T> BinaryOperator<T> thereCanBeOnlyOne()
{
    return (a, b) -> {throw new RuntimeException("Duplicate elements found: " + a + " and " + b);};
}

Vì vậy, đối với trường hợp với Userbạn sẽ có:

User match = users.stream().filter((user) -> user.getId() < 0).reduce(thereCanBeOnlyOne()).get();

8

Sử dụng giảm

Đây là cách đơn giản và linh hoạt hơn mà tôi tìm thấy (dựa trên câu trả lời @prunge)

Optional<User> user = users.stream()
        .filter(user -> user.getId() == 1)
        .reduce((a, b) -> {
            throw new IllegalStateException("Multiple elements: " + a + ", " + b);
        })

Bằng cách này bạn có được:

  • Tùy chọn - như mọi khi với đối tượng của bạn hoặc Optional.empty() nếu không có
  • Ngoại lệ (cuối cùng là loại / thông báo tùy chỉnh CỦA BẠN) nếu có nhiều hơn một yếu tố

6

Tôi nghĩ cách này đơn giản hơn:

User resultUser = users.stream()
    .filter(user -> user.getId() > 0)
    .findFirst().get();

4
Nó chỉ tìm thấy đầu tiên nhưng trường hợp cũng là ném Exception khi nó còn hơn một
lczapski

5

Sử dụng một Collector:

public static <T> Collector<T, ?, Optional<T>> toSingleton() {
    return Collectors.collectingAndThen(
            Collectors.toList(),
            list -> list.size() == 1 ? Optional.of(list.get(0)) : Optional.empty()
    );
}

Sử dụng:

Optional<User> result = users.stream()
        .filter((user) -> user.getId() < 0)
        .collect(toSingleton());

Chúng tôi trả lại một Optional, vì chúng tôi thường không thể giả sử Collectioncó chứa chính xác một yếu tố. Nếu bạn đã biết đây là trường hợp, hãy gọi:

User user = result.orElseThrow();

Điều này đặt gánh nặng xử lý lỗi lên người gọi - như thường lệ.



1

Chúng tôi có thể sử dụng RxJava ( thư viện mở rộng phản ứng rất mạnh )

LinkedList<User> users = new LinkedList<>();
users.add(new User(1, "User1"));
users.add(new User(2, "User2"));
users.add(new User(3, "User3"));

User userFound =  Observable.from(users)
                  .filter((user) -> user.getId() == 1)
                  .single().toBlocking().first();

Các đơn điều hành ném một ngoại lệ nếu không có người dùng hoặc sau đó thêm một người dùng được tìm thấy.


Câu trả lời đúng, khởi tạo một luồng chặn hoặc bộ sưu tập có lẽ không phải là rất rẻ (về tài nguyên) mặc dù.
Karl Richter

1

Collectors.toMap(keyMapper, valueMapper)sử dụng một sự hợp nhất ném để xử lý nhiều mục với cùng một khóa, thật dễ dàng:

List<User> users = new LinkedList<>();
users.add(new User(1, "User1"));
users.add(new User(2, "User2"));
users.add(new User(3, "User3"));

int id = 1;
User match = Optional.ofNullable(users.stream()
  .filter(user -> user.getId() == id)
  .collect(Collectors.toMap(User::getId, Function.identity()))
  .get(id)).get();

Bạn sẽ nhận được một IllegalStateExceptionkhóa trùng lặp. Nhưng cuối cùng, tôi không chắc liệu mã có thể dễ đọc hơn nữa hay không if.


1
Giải pháp tốt! Và nếu bạn làm .collect(Collectors.toMap(user -> "", Function.identity())).get(""), bạn có một hành vi chung chung hơn.
glglgl

1

Tôi đang sử dụng hai bộ sưu tập đó:

public static <T> Collector<T, ?, Optional<T>> zeroOrOne() {
    return Collectors.reducing((a, b) -> {
        throw new IllegalStateException("More than one value was returned");
    });
}

public static <T> Collector<T, ?, T> onlyOne() {
    return Collectors.collectingAndThen(zeroOrOne(), Optional::get);
}

Khéo léo! onlyOne()ném IllegalStateExceptioncho> 1 phần tử và NoSuchEuityException` (in Optional::get) cho 0 phần tử.
simon04

@ simon04 Bạn có thể quá tải các phương pháp để có một Suppliercủa (Runtime)Exception.
Xavier Dury

1

Nếu bạn không phiền khi sử dụng thư viện của bên thứ 3, SequenceMtừ các luồng cyclops (và LazyFutureStreamtừ phản ứng đơn giản ), cả hai đều có các toán tử đơn & đơn lẻ.

singleOptional()ném một ngoại lệ nếu có 0hoặc nhiều hơn 1các phần tử trong Stream, nếu không, nó sẽ trả về giá trị đơn.

String result = SequenceM.of("x")
                          .single();

SequenceM.of().single(); // NoSuchElementException

SequenceM.of(1, 2, 3).single(); // NoSuchElementException

String result = LazyFutureStream.fromStream(Stream.of("x"))
                          .single();

singleOptional()trả về Optional.empty()nếu không có giá trị hoặc nhiều hơn một giá trị trong Stream.

Optional<String> result = SequenceM.fromStream(Stream.of("x"))
                          .singleOptional(); 
//Optional["x"]

Optional<String> result = SequenceM.of().singleOptional(); 
// Optional.empty

Optional<String> result =  SequenceM.of(1, 2, 3).singleOptional(); 
// Optional.empty

Tiết lộ - Tôi là tác giả của cả hai thư viện.


0

Tôi đã đi với cách tiếp cận trực tiếp và chỉ thực hiện điều:

public class CollectSingle<T> implements Collector<T, T, T>, BiConsumer<T, T>, Function<T, T>, Supplier<T> {
T value;

@Override
public Supplier<T> supplier() {
    return this;
}

@Override
public BiConsumer<T, T> accumulator() {
    return this;
}

@Override
public BinaryOperator<T> combiner() {
    return null;
}

@Override
public Function<T, T> finisher() {
    return this;
}

@Override
public Set<Characteristics> characteristics() {
    return Collections.emptySet();
}

@Override //accumulator
public void accept(T ignore, T nvalue) {
    if (value != null) {
        throw new UnsupportedOperationException("Collect single only supports single element, "
                + value + " and " + nvalue + " found.");
    }
    value = nvalue;
}

@Override //supplier
public T get() {
    value = null; //reset for reuse
    return value;
}

@Override //finisher
public T apply(T t) {
    return value;
}


} 

với bài kiểm tra JUnit:

public class CollectSingleTest {

@Test
public void collectOne( ) {
    List<Integer> lst = new ArrayList<>();
    lst.add(7);
    Integer o = lst.stream().collect( new CollectSingle<>());
    System.out.println(o);
}

@Test(expected = UnsupportedOperationException.class)
public void failOnTwo( ) {
    List<Integer> lst = new ArrayList<>();
    lst.add(7);
    lst.add(8);
    Integer o = lst.stream().collect( new CollectSingle<>());
}

}

Việc thực hiện này không an toàn.


0
User match = users.stream().filter((user) -> user.getId()== 1).findAny().orElseThrow(()-> new IllegalArgumentException());

5
Mặc dù mã này có thể giải quyết câu hỏi, bao gồm giải thích về cách thức và lý do giải quyết vấn đề này thực sự sẽ giúp cải thiện chất lượng bài đăng của bạn và có thể dẫn đến nhiều lượt bình chọn hơn. Hãy nhớ rằng bạn đang trả lời câu hỏi cho độc giả trong tương lai, không chỉ người hỏi bây giờ. Vui lòng chỉnh sửa câu trả lời của bạn để thêm giải thích và đưa ra dấu hiệu về những hạn chế và giả định được áp dụng.
David Buck

-2

Bạn đã thử cái này chưa

long c = users.stream().filter((user) -> user.getId() == 1).count();
if(c > 1){
    throw new IllegalStateException();
}

long count()
Returns the count of elements in this stream. This is a special case of a reduction and is equivalent to:

     return mapToLong(e -> 1L).sum();

This is a terminal operation.

Nguồn: https://docs.oracle.com/javase/8/docs/api/java/util/stream/Stream.html


3
Người ta nói rằng count()nó không tốt để sử dụng vì nó là một hoạt động đầu cuối.
ry Bổ trợ

Nếu đây thực sự là một trích dẫn, vui lòng thêm các nguồn của bạn
Neuron
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.