• 初识Java 12-2 流


    目录

    中间操作

    跟踪与调试

    对流元素进行排序

    移除元素

    将函数应用于每个流元素

    在应用map()期间组合流

    Optional类型

    便捷函数

    创建Optional

    Optional对象上的操作

    由Optional组成的流


    本笔记参考自: 《On Java 中文版》


    中间操作

    ||| 中间操作:从流中接收一个对象,并将对象作为另一个流送出后端,以连接其他操作。

    跟踪与调试

            peek()操作是用于辅助调试的,它允许我们查看流对象而不修改它。

    【例子:peek()的使用流】

    1. public class Peeking {
    2. public static void main(String[] args)
    3. throws Exception {
    4. FileToWords.stream("Cheese.dat")
    5. .skip(21)
    6. .limit(4)
    7. .map(w -> w + " ")
    8. .peek(System.out::print) // 查看当前流的状态
    9. .map(String::toUpperCase)
    10. .peek(System.out::print)
    11. .map(String::toLowerCase)
    12. .forEach(System.out::print);
    13. }
    14. }

            程序执行的结果是:

            其中,FileToWords类的形式如下(之后会再展示):

    这个类的作用就是生成一个String对象组成的流。

            当流在管线中传输时,我们使用了peek()进行观察。这里要注意的是,peek()接受的是一个遵循Consumer函数式接口的函数:

    这样的函数没有返回值Consumer一般用于对输入元素进行消费),所以也就不可能使用不同的对象替换流中的对象。因此我们只能“观察”这些对象。


    对流元素进行排序

            sorted()方法可用于流的排序,它有一种可以接受一个Comparator参数的形式:

    【例子:接受参数的sorted()

    1. import java.util.Comparator;
    2. public class SortedComparator {
    3. public static void main(String[] args)
    4. throws Exception {
    5. FileToWords.stream("D:\\code\\Java\\Test_Java\\src\\streams\\Cheese.dat")
    6. .skip(10)
    7. .limit(10)
    8. .sorted(Comparator.reverseOrder())
    9. .map(w -> w + " ")
    10. .forEach(System.out::print);
    11. System.out.println();
    12. }
    13. }

            程序执行的结果是:

            也可以传入一个lambda表达式作为sorted()的参数,不过也有许多预设的Comparator


    移除元素

            介绍两个用于从流中移除元素的操作:

    • distinct():可用于移除流中的重复元素。与Set相比,distinct()要更加简洁。
    • filter(Predicate):过滤,只保留符合特定条件(结果为true)的元素。

            distinct()笔记12-1Randoms.java中出现过。这里演示filter(Predicate)操作:

    【例子:filter(Predicate)的使用例】

    1. import java.util.stream.LongStream;
    2. import static java.util.stream.LongStream.iterate;
    3. import static java.util.stream.LongStream.rangeClosed;
    4. public class Prime {
    5. public static boolean isPrime(long n) {
    6. return rangeClosed(2, (long) Math.sqrt(n)) // rangeClosed()在这里是类似于循环的作用
    7. .noneMatch(i -> n % i == 0); // 若流为空,或流中没有元素能够与当前谓词匹配,返回true
    8. }
    9. public LongStream numbers() {
    10. return iterate(2, i -> i + 1)
    11. .filter(Prime::isPrime); // 若返回结果是true,则保留下来
    12. }
    13. public static void main(String[] args) {
    14. new Prime().numbers()
    15. .limit(10)
    16. .forEach(n -> System.out.format("%d ", n));
    17. System.out.println();
    18. new Prime().numbers()
    19. .skip(90)
    20. .limit(10)
    21. .forEach(n -> System.out.format("%d ", n));
    22. }
    23. }

            程序执行的结果是:

            rangeClosed()的参数是一个左开右闭的区间,包含了上界值。若没有一个取余操作的结果是0,则noneMatch()返回true,否则返回false。并且noneMatch()会在第一次结果为false时退出。


    将函数应用于每个流元素

            本条目用于介绍一些作用于每个流元素的各种map操作:

    • map(Function):将Function应用于输入流中的每个对象,结果作为输出流继续传递。

            以下是map面对不同类型的不同版本:

    • mapToInt(ToIntFunction):应用于IntStream中。
    • mapToLong(ToLongFunction):应用于LongStream中。
    • mapToDouble(ToDoubleFunction):应用于LongStream中。

    【例子:将Function(通过map)映射到一个用String组成的流中】

    1. import java.util.Arrays;
    2. import java.util.function.Function;
    3. import java.util.stream.Stream;
    4. public class FunctionMap {
    5. static String[] elements = {"12", "", "23", "45"};
    6. static Stream testStream() {
    7. return Arrays.stream(elements);
    8. }
    9. static void test(String descr, Function func) {
    10. System.out.println(" ---( " + descr + " )---");
    11. testStream()
    12. .map(func)
    13. .forEach(System.out::println);
    14. }
    15. public static void main(String[] args) {
    16. test("添加括号", s -> "[" + s + "]");
    17. test("数值增长", s -> {
    18. try {
    19. return Integer.parseInt(s) + 1 + ""; // parseInt()会尝试将String转换为Integer
    20. } catch (NumberFormatException e) { // 若无法完成转换,抛出异常,执行catch{}中的语句
    21. return s;
    22. }
    23. });
    24. test("替换", s -> s.replace("2", "9"));
    25. test("提取最后一位的数值",
    26. s -> s.length() > 0 ?
    27. s.charAt(s.length() - 1) + "" : s);
    28. }
    29. }

            程序执行的结果是:

             在语句

    return Integer.parseInt(s) + 1 + "";

    中,表达式从左向右顺序进行,①先尝试将s转换为Integer,②再进行+1操作,③最后将其转换为字符串。

            另外,String.charAt()会返回指定索引处的char值。索引的范围是从0String.length()-1

    ---

            上述程序中,map()被用于将一个String映射到另一个String。而实际上,并没有约束要求生成的类型必须和输入的类型一致。因此还可以在map()中改变这个流的类型:

    【例子:将基本类型的流映射到自定义类型中】

            在这个例子中,我们接受的是一个int类型的流,并且使用Numbered::new将其转换为了Numbered

    ---

            若Function生成的结果是某种数值类型,就需要使用相应的mapTo操作:

    【例子:使用mapTo处理数值类型】

    1. import java.util.stream.Stream;
    2. public class FunctionMap3 {
    3. public static void main(String[] args) {
    4. Stream.of("5", "7", "9")
    5. .mapToInt(Integer::parseInt)
    6. .forEach(n -> System.out.format("%d ", n));
    7. System.out.println();
    8. Stream.of("17", "19", "23")
    9. .mapToLong(Long::parseLong)
    10. .forEach(n -> System.out.format("%d ", n));
    11. System.out.println();
    12. Stream.of("17", "1.9", ".23")
    13. .mapToDouble(Double::parseDouble)
    14. .forEach(n -> System.out.format("%f ", n));
    15. }
    16. }

            程序执行的结果是:


    在应用map()期间组合流

            假设我们现在有得到了一个由传入元素组成的流,我们需要对其运用map()

    我们希望得到的是一个String类型的流,但结果却是一个由指向其他流的“头”组成的流。换句话说,我们想要的是一个由元素组成的流,但却生成了一个与元素流组成的流

            这种时候就需要使用flatMap()。该方法会做两件事:

    1. 接受生成流的函数,并将其应用于传入元素(这一点和map()一样)。
    2. 然后,将每个流“扁平化”处理,将其展开为元素。

        和map()类似,flatMap()也有面对不同类型的不同版本(flatMapToInt()flatMapToLong()等)。

    【例子:flatMap()的使用例】

    1. import java.util.stream.Stream;
    2. public class FlatMap {
    3. public static void main(String[] args) {
    4. Stream.of(1, 2, 3)
    5. .flatMap(i -> Stream.of("A", "B", "C"))
    6. .forEach(System.out::println);
    7. }
    8. }

            程序执行的结果是:

            从映射返回的每个流都被自动进行了扁平化处理,展开为组成这个流的String元素。

    【例子:flatMapTo生成随机数】

    1. import java.util.Random;
    2. import java.util.stream.IntStream;
    3. import java.util.stream.Stream;
    4. public class StreamOfRandoms {
    5. static Random rand = new Random(47);
    6. public static void main(String[] args) {
    7. Stream.of(1, 2, 3, 4, 5)
    8. .flatMapToInt(i -> IntStream.concat( // concat()会按照参数的顺序将两个流组合到一起
    9. rand.ints(0, 100).limit(i), // 其中的每一个流的长度不超过i
    10. IntStream.of(-1)
    11. ))
    12. .forEach(n -> System.out.format("%d ", n));
    13. System.out.println();
    14. }
    15. }

            程序执行的结果是:

            上述程序中出现了concat()

    这个方法会按照参数的顺序将两个流组合到一起。所以,在每个有随机的Integer组成的流的末尾,都添加了一个-1作为标记。

        因为rand.ints()生成的是一个IntStream,所以这里使用的flatMap()concat()of()都是Integer的版本。

            在笔记12-1FileToWordsRegexp.java中,我们将整个文件读取到内存的List中,这是需要存储空间的。但我们真正需要的,是一个不需要中间存储的单词流。这就是flatMap()解决的问题:

    【例子:flatMap()创建占用少量内存的流】

    1. import java.nio.file.Files;
    2. import java.nio.file.Paths;
    3. import java.util.regex.Pattern;
    4. import java.util.stream.Stream;
    5. public class FileToWords {
    6. public static Stream stream(String filepath)
    7. throws Exception {
    8. return Files.lines(Paths.get(filepath))
    9. .flatMap(line -> Pattern.compile("\\W+").splitAsStream(line));
    10. }
    11. }

            stream()在这里是一个静态方法,因为它自己就可以完成整个流的创建。

            这里还出现了正则表达式\\W+\\W的意思是一个“非单词字符”,而+意味着“一个或是多个”。

        小写形式的\\w是指“单词字符”。

            由于语句

    Pattern.compile().splitAsStream()

    会生成一个流,所以若将其应用于map()操作中,我们会得到的是一个由单词流组成的流。而我们仅仅需要一个单词流而已。因此这里需要使用的是flatMap(),将其转化为一个由元素组成的简单流。

            除此之外,我们还可以使用String.split(),它会生成一个数组,这个数组可以经由Arrays.stream()转换为一个流:

    .flatMap(line -> Arrays.stream(line.split("\\W+")))

    此时得到的是一个真正的流(而不是像FileToWordsRegexp.java中那样,基于集合创建的流)。因此,每当我们想要一个新的流时,就必须从头创建,并且它无法复用

    【例子:无法复用的流】

    1. public class FileToWordsTest {
    2. public static void main(String[] args)
    3. throws Exception {
    4. FileToWords.stream("Cheese.dat")
    5. .limit(7)
    6. .forEach(s -> System.out.format("%s ", s));
    7. System.out.println();
    8. FileToWords.stream("Cheese.dat")
    9. .skip(7)
    10. .limit(2)
    11. .forEach(s -> System.out.format("%s ", s));
    12. }
    13. }

            程序执行的结果如下:

    Optional类型

            在进行编程时,我们或许会认为流被连接成了一条“快乐通道”(指没有异常或错误情形发生的默认场景),并假设没有什么能够中断这个流。但事实是,只需要在流中放入一个null就可以破坏它。

        当从流中提取null时,对null进行的类型转换会发生异常。

            因此就需要Optional类型。这一类型实现了这样一个概念:存在一个对象,即可以作为流元素来占位,也可以在需要寻找的元素不存在时提醒我们(不会抛出异常)。

            某些标准的流操作会返回Optional对象,因为这些操作不能确保所要的结果一定存在:

    • findFirst():返回包含第一个元素的Optional
    • findAny():返回包含任何元素的Optional
    • max()min():返回包含流中最大值或最小值的Optional

            以上三种操作在流为空的时候都返回Optional.empty

    • reduce()的其中一个版本:这个版本不会把一个“identity”对象作为其第一个参数(其他版本中,“identity”是默认结果),

      它的返回值会被包在一个Optional中。
    • average():将数值化的流(IntStreamLongStreamDoubleStream)包在一个Optional中。

    【例子:返回Optional类型的操作】

    1. import java.util.stream.IntStream;
    2. import java.util.stream.Stream;
    3. public class OptionalsFromEmptyStreams {
    4. public static void main(String[] args) {
    5. System.out.println(Stream.empty()
    6. .findFirst());
    7. System.out.println(Stream.empty()
    8. .findAny());
    9. System.out.println(Stream.empty()
    10. .max(String.CASE_INSENSITIVE_ORDER));
    11. System.out.println(Stream.empty()
    12. .min(String.CASE_INSENSITIVE_ORDER));
    13. System.out.println(Stream.empty()
    14. .reduce((s1, s2) -> s1 + s2));
    15. System.out.println(IntStream.empty()
    16. .average());
    17. }
    18. }

            程序执行的结果是:

            此时得到的结果不是抛出的异常,而是Optional.empty()对象。

            注意:这里通过Stream.empty()创建了空流。若使用的是Stream.empty(),那么Java就无法通过这么有限的上下文信息推断出这个流的类型,但这种语法解决了这一问题:

    Stream. s = Stream.empty();

    【例子:Optional的两个基本动作】

    1. import java.util.Optional;
    2. import java.util.stream.Stream;
    3. public class OptionalBasics {
    4. static void test(Optional optString) {
    5. if (optString.isPresent()) // isPresent():若存在值,返回true
    6. System.out.println(optString.get());
    7. else
    8. System.out.println("流中不存在数据");
    9. }
    10. public static void main(String[] args) {
    11. test(Stream.of("一个元素").findFirst());
    12. test(Stream.empty().findFirst());
    13. }
    14. }

            程序执行的结果是:

            当接收了一个Optional时,首先调用了isPresent(),对流中元素的存在与否进行测试。

    便捷函数

            有许多便捷函数,由于获取Optional中的数据。这些函数简化了上述例子的“先检查、再处理”的过程。

    • isPresent():若存在值,返回true。否则返回false。
    • ifPresent(Consumer):若值存在,则使用这个值调用Consumer。否则不进行任何动作。
    • orElse(otherObject):若对象存在,则返回这个对象。否则返回otherObject
    • orElseGet(Supplier):若对象存在,则返回这个对象。否则返回使用Supplier函数创建的替代对象。
    • orElseThrow(Supplier):若对象存在,则返回这个对象。否则抛出一个使用Supplier函数创建的异常。

    【例子:便捷函数】

    1. import java.util.Optional;
    2. import java.util.function.Consumer;
    3. import java.util.stream.Stream;
    4. public class Optionals {
    5. static void basics(Optional optString) {
    6. if (optString.isPresent())
    7. System.out.println(optString.get());
    8. else
    9. System.out.println("流中不存在数据");
    10. }
    11. static void ifPresent(Optional optString) {
    12. optString.ifPresent(System.out::println);
    13. }
    14. static void orElse(Optional optString) {
    15. System.out.println(optString.orElse("从orElse()返回的对象"));
    16. }
    17. static void orElseGet(Optional optString) {
    18. System.out.println(optString.orElseGet(() -> "orElseGet()替换的对象"));
    19. }
    20. static void orElseThrow(Optional optString) {
    21. try {
    22. System.out.println(optString.orElseThrow(
    23. () -> new Exception("orElseThrow()替换的对象")
    24. ));
    25. } catch (Exception e) { // 使用catch捕获Java库函数Optional::orElseThrow()抛出的异常
    26. System.out.println("捕获 " + e);
    27. }
    28. }
    29. static void test(String testName
    30. , Consumer> cos) { // 一个可以接收所有示例方法的Consumer,避免代码重复
    31. System.out.println(" === " + testName + " === ");
    32. cos.accept(Stream.of("测试语句").findFirst());
    33. cos.accept(Stream.empty().findFirst());
    34. }
    35. public static void main(String[] args) {
    36. test("basics", Optionals::basics);
    37. test("ifPresent", Optionals::ifPresent);
    38. test("orElse", Optionals::orElse);
    39. test("orElseGet", Optionals::orElseGet);
    40. test("orElseThrow", Optionals::orElseThrow);
    41. }
    42. }

            程序执行的结果是:


    创建Optional

            若要自己编写生成Optional的代码,可以使用以下三种静态方法:

    • empty():返回一个空的Optional
    • of(value):若已经确定这个value不是null,可以通过该方法将其包在一个Optional中。
    • ofNullable(value):若确定value是否为null,使用这个方法。若valuenull,该方法返回Optional.empty(),否则将这个value包在一个Optional中。

    【例子:生成Optional的三个方法】

    1. import java.util.Optional;
    2. public class CreatingOptionals {
    3. static void test(String testName, Optional opt) {
    4. System.out.println(" === " + testName + " === ");
    5. System.out.println(opt.orElse("Null"));
    6. }
    7. public static void main(String[] args) {
    8. test("empty", Optional.empty());
    9. test("of", Optional.of("Value"));
    10. try {
    11. test("of", Optional.of(null));
    12. } catch (Exception e) {
    13. System.out.println(e);
    14. }
    15. test("ofNullable", Optional.ofNullable("Value"));
    16. test("ofNullable", Optional.ofNullable(null));
    17. }
    18. }

            程序执行的结果如下:

            若试图通过of()传递null来创建Optional,就会抛出空指针异常。相比之下,ofNullable()显得更加安全。


    Optional对象上的操作

            若生成了一个Optional,有三种方法可以在最后再做一项处理:

    • filter(Predicate):将Predicate应用于Optional的内容,并返回其结果。若OptionalPredicate不匹配,则返回empty。若Optionalempty,返回其本身。
    • map(Function):若Optional不是empty,将Function应用于Optional包含的对象,并返回结果。否则返回empty
    • flatMap(Function):与map()类似,但所提供的映射函数会将结果包在Optional中,这样flatMap()最后就不会再做任何包装了。

        但是,数值化的Optional上没有上述的这些操作。

            filter()方法存在于普通的流中和在Optional中。但它们的行为并不相同,在普通的流中,若Predicate返回falsefilter()会将元素从流中删除。而Optional中的filter()则会将其转换为empty

    【例子:filter()的用法】

    1. import java.util.Arrays;
    2. import java.util.function.Predicate;
    3. import java.util.stream.Stream;
    4. public class OptionalFilter {
    5. static String[] elements = {
    6. "Foo", "", "Bar", "Baz", "Bingo"
    7. };
    8. static Stream testStream() {
    9. return Arrays.stream(elements);
    10. }
    11. static void test(String descr, Predicate pred) {
    12. System.out.println(" ---( " + descr + " )---");
    13. for (int i = 0; i <= elements.length; i++) {
    14. System.out.println(
    15. testStream() // 注意:每次进入for循环,都会重新获取一个流
    16. .skip(i)
    17. .findFirst()
    18. .filter(pred)
    19. );
    20. }
    21. }
    22. public static void main(String[] args) {
    23. test("true", str -> true);
    24. test("false", str -> false);
    25. test("str != \"\"", str -> str != "");
    26. test("str.length() == 3", str -> str.length() == 3);
    27. test("startsWith(\"B\")", str -> str.startsWith("B"));
    28. }
    29. }

            程序执行的结果是:

            这次的程序虽然输出结果像一个流,但实际上每次进入for循环都会重新获取一个流。只是skip()操作会跳过元素,以至于结果看上去是一个流。

            这次for循环语句的结束条件并不是i < elements.length,而是i <= elements.length。因此最后一个元素事实上会超出这个流,但超出的部分自动变为了一个Optional.empty

            Optional也有自己的map()。不同的是,只有当Optional不为empty时,map()才会应用其的映射函数。

        若Optional不为空,则将其传递给函数时,map()会首先提取Optional中的对象。

    【例子:Optional.map()的使用例】

    1. import java.util.Arrays;
    2. import java.util.function.Function;
    3. import java.util.stream.Stream;
    4. public class OptionalMap {
    5. static String[] elements = {"12", "", "23", "45"};
    6. static Stream testStream() {
    7. return Arrays.stream(elements);
    8. }
    9. static void test(String descr,
    10. Function func) {
    11. System.out.println(" ---( " + descr + " )---");
    12. for (int i = 0; i <= elements.length; i++) {
    13. System.out.println(
    14. testStream()
    15. .skip(i)
    16. .findFirst() // 生成一个Optional
    17. .map(func)
    18. );
    19. }
    20. }
    21. public static void main(String[] args) {
    22. test("加上括号", s -> "[" + s + "]");
    23. test("递增", s -> {
    24. try {
    25. return Integer.parseInt(s) + 1 + "";
    26. } catch (NumberFormatException e) {
    27. return s;
    28. }
    29. });
    30. test("替换", s -> s.replace("2", "9"));
    31. test("获取最后一位数字", s -> s.length() > 0 ?
    32. s.charAt(s.length() - 1) + "" : s);
    33. }
    34. }

            程序执行的结果是:

            在函数完成之后,map()会先把结果包在一个Optional中,然后返回。并且,Optional.empty在遇到map()时直接通过,并没有被更改。

                    OptionalflatMap()被应用于已经会生成Optional的映射函数,所以flatMap()并没有像map()一样进行包装的操作:

    【例子:Optional.flatMap()的使用例】

    1. import java.util.Arrays;
    2. import java.util.Optional;
    3. import java.util.function.Function;
    4. import java.util.stream.Stream;
    5. public class OptionalFlatMap {
    6. static String[] elements = {"12", "", "23", "45"};
    7. static Stream testStream() {
    8. return Arrays.stream(elements);
    9. }
    10. static void test(String descr,
    11. Function> func) { // Function的返回类型是Optional
    12. System.out.println(" ---( " + descr + " )---");
    13. for (int i = 0; i <= elements.length; i++) {
    14. System.out.println(
    15. testStream()
    16. .skip(i)
    17. .findFirst()
    18. .flatMap(func)
    19. );
    20. }
    21. }
    22. public static void main(String[] args) {
    23. // 用Optional.of()将函数括起来
    24. test("加上括号", s -> Optional.of("[" + s + "]"));
    25. test("递增", s -> {
    26. try {
    27. return Optional.of(Integer.parseInt(s) + 1 + "");
    28. } catch (NumberFormatException e) {
    29. return Optional.of(s);
    30. }
    31. });
    32. test("替换", s -> Optional.of(s.replace("2", "9")));
    33. test("获取最后一位数字", s -> Optional.of(s.length() > 0 ?
    34. s.charAt(s.length() - 1) + "" : s));
    35. }
    36. }

            程序执行的结果是:

            flatMap()map()唯一的区别在于,flatMap()不会将结果包在Optional中,这件事会交由映射函数来做。


    由Optional组成的流

            Optional可以处理null值。所以若存在一个可能会生成null值的生成器,并且这个生成器创建了一个流,我们自然会想要将这些元素包含在Optional中。

    【例子:由Optional组成的流】

    1. import java.util.Optional;
    2. import java.util.Random;
    3. import java.util.stream.Stream;
    4. public class Signal {
    5. private final String msg;
    6. public Signal(String msg) {
    7. this.msg = msg;
    8. }
    9. public String getMsg() {
    10. return msg;
    11. }
    12. @Override
    13. public String toString() {
    14. return "Signal(" + msg + ")";
    15. }
    16. static Random rand = new Random(47);
    17. public static Signal morse() {
    18. switch (rand.nextInt(4)) {
    19. case 1:
    20. return new Signal("dot");
    21. case 2:
    22. return new Signal("dash");
    23. default:
    24. return null;
    25. }
    26. }
    27. public static Stream> stream() {
    28. return Stream.generate(Signal::morse)
    29. .map(signal -> Optional.ofNullable(signal)); // 使用ofNullable将元素包入Optional中
    30. }
    31. }

            当我们需要使用这个流的时候,我们需要考虑如何获取Optional中的对象:

    【例子:从Optional的流中获取对象】

    1. import java.util.Optional;
    2. public class StreamOfOptionals {
    3. public static void main(String[] args) {
    4. Signal.stream()
    5. .limit(10)
    6. .forEach(System.out::println);
    7. System.out.println(" ---");
    8. Signal.stream()
    9. .limit(10)
    10. .filter(Optional::isPresent)
    11. .map(Optional::get)
    12. .forEach(System.out::println);
    13. }
    14. }

            程序执行的结果如下:

            从Optional的流中提取对象时,往往会遇到“没有值”的情况。这就需要我们针对不同的应用采取不同的方法。

  • 相关阅读:
    Mysql索引Hash和BTree的区别
    微软发布Phi-3,手机上就能跑,是时候聊聊小型语言模型了|TodayAI
    Chatgpt人工智能对话源码系统分享 带完整搭建教程
    线程相关的模型
    1Panel 升级 Halo报错
    MySQL-操作数据库(存储引擎)
    [附源码]计算机毕业设计JAVA个性化新闻推荐系统
    泛型的介绍
    【漏洞复现-webmin-命令执行】vulfocus/webmin-cve_2019_15107
    数据分析之AB测试
  • 原文地址:https://blog.csdn.net/w_pab/article/details/133466685