JavaStream操作这么多,其实只有两大类,看完这篇就清晰了
本节内容是 Stream API 中常用操作的学习和理解,下面会专门再有一篇文章介绍在项目开发中那些高频使用的利用 Stream 处理对象集合的使用示例。
本文大纲如下:
Java 的 Stream API 提供了一种处理对象集合的函数式方法。 Stream 是 Lambda 表达式等其他几个函数式编程特性一起在 Java 8 被引入的这个篇教程将解释这些函数式 Stream 是如何工作的,以及怎么使用它们。
注意,Java 的 Stream API 与 Java IO 的 InputStream 和 OutputStream 没有任何关系,不要因为名字类似造成误解。 InputStream 和 OutputStream 是与字节流有关,而 Java 的 Stream API 用于处理对象流。Stream 的定义
Java 的 Stream 是一个能够对其元素进行内部迭代的组件,这意味着它可以自己迭代其元素。相反地,当我们使用 Collection 的迭代功能,例如,从 Collection 获取Iterator 或者使用 Iterable 接口 的 forEach 方法这些方式进行迭代时,我们必须自己实现集合元素的迭代逻辑。
当然集合也支持获取 Stream 完成迭代,这些我们在介绍集合框架的相关章节都介绍过。流处理
我们可以将 Listener 方法或者叫处理器方法附加到 Stream 上。当 Stream 在内部迭代元素时,将以元素为参数调用这些处理器。Stream 会为流中的每个元素调用一次处理器。所以每个处理器方法都可以处理 Stream 中的每个元素,我们把这称为流处理。
流的多个处理器方法可以形成一个调用链。链上的前一个处理器处理流中的元素,返回的新元素会作为参数传给链中的下一个处理器处理。当然,处理器可以返回相同的元素或新元素,具体取决于处理器的目的和用途。怎么获取流
有很多方法获取 Stream ,一般最常见的是从 Collection 对象中获取 Stream。下面是一个从 List 对象获取 Stream 的例子。List items = new ArrayList(); items.add("one"); items.add("two"); items.add("three"); Stream stream = items.stream(); 复制代码
集合对象都实现了 Collection 接口,所以通过接口里定义的 stream 方法获救获取到由集合元素构成的 Steam。流处理的构成
在对流进行处理时,不同的流操作以级联的方式形成处理链。一个流的处理链由一个源(source),0 到多个中间操作(intermediate operation)和一个终结操作(terminal operation)完成。源:源代表 Stream 中元素的来源,比如我们上面看到的集合对象。中间操作:中间操作,在一个流上添加的处理器方法,他们的返回结果是一个新的流。这些操作是延迟执行的,在终结操作启动后才会开始执行。终结操作:终结流操作是启动元素内部迭代、调用所有处理器方法并最终返回结果的操作。
概念听起来有点模糊,我们通过流处理的例子再理解一下。import java.util.ArrayList; import java.util.List; import java.util.stream.Stream; public class StreamExamples { public static void main(String[] args) { List stringList = new ArrayList<>(); stringList.add("ONE"); stringList.add("TWO"); stringList.add("THREE"); Stream stream = stringList.stream(); long count = stream .map((value) -> value.toLowerCase()) .count(); System.out.println("count = " + count); } } 复制代码
map() 方法的调用是一个中间操作。它只是在流上设置一个 Lambda 表达式,将每个元素转换为小写形式。而对 count() 方法的调用是一个终结操作。此调用会在内部启动迭代,开始流处理,这将导致每个元素都转换为小写然后计数。
将元素转换为小写实际上并不影响元素的计数。转换部分只是作为 map() 是一个中间操作的示例。流的中间操作
Stream API 的中间(非终结)流操作是转换或者过滤流中元素的操作。当我们把中间操作添加到流上时,我们会得到一个新的流作为结果。下面是一个添加到流上的中间操作的示例,它的执行结果会产生一个新的流。List stringList = new ArrayList<>(); stringList.add("ONE"); stringList.add("TWO"); stringList.add("THREE"); Stream stream = stringList.stream(); Stream stringStream = stream.map((value) -> value.toLowerCase()); 复制代码
上面例子中,流上添加的 map() 调用,此调用实际上返回一个新的 Stream 实例,该实例表示原始字符串流应用了 map 操作后的新流。 只能将单个操作添加到给定的 Stream 实例上。如果需要将多个操作连接在一起,则只能将第二个操作应用于第一个操作产生的 Stream 实例上。Stream stringStream1 = stream.map((value) -> value.toLowerCase()); Stream<½String> stringStream2 = stringStream1.map((value) -> value.toUpperCase()); 复制代码
注意第二个 map() 调用是如何在第一个 map() 调用返回的 Stream 上进行调用的。
我们一般是将 Stream 上的所有中间操作串联成一个调用链:Stream stream1 = stream .map((value) -> value.toLowerCase()) .map((value) -> value.toUpperCase()) .map((value) -> value.substring(0,3)); 复制代码
以 map方法为代表流间操作方法的参数,是一个函数式接口,我们可以直接用 Lambda 表达式作为这些操作的参数。所以在介绍 Lambda 的那一节我们也说过,Lambda 一般是和流操作就结合起来用的。
**参考--Java 的函数式接口: **tutorials.jenkov.com/java-functi…
下面我们说一下常用的流的中间操作。map
map() 方法将一个元素转换(或者叫映射)到另一个对象。例如,一个字符串列表,map() 可以将每个字符串转换为小写、大写或原始字符串的子字符串,或完全不同的东西。List list = new ArrayList(); Stream stream = list.stream(); Stream streamMapped = stream.map((value) -> value.toUpperCase()); 复制代码filter
filter() 用于从 Stream 中过滤掉元素。 filter 方法接受一个 Predicate (也是一个函数式接口),filter() 为流中的每个元素调用 Predicate。如果元素要包含在 filter() 返回结果的流中,则 Predicate 应返回 true。如果不应包含该元素,则 Predicate 应返回 false。Stream longStringsStream = stream.filter((value) -> { // 元素长度大于等于3,返回true,会被保留在 filter 产生的新流中。 return value.length() >= 3; }); 复制代码
比如 Stream 实例应用了上面这个 filter 后,filter 返回的结果流里只会包含长度不小于 3 的元素。flatMap
flatMap方法接受一个 Lambda 表达式, Lambda 的返回值必须也是一个stream类型,flatMap方法最终会把所有返回的stream合并。map 与 flatMap 方法很像,都是以某种方式转换流中的元素。如果需要将每个元素转换为一个值,则使用 map 方法,如果需要将每个元素转换为多个值组成的流,且最终把所有元素的流合并成一个流,则需要使用 flatMap 方法。 在效果上看是把原来流中的每个元素进行了"展平"import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.stream.Stream; public class StreamFlatMapExamples { public static void main(String[] args) { List stringList = new ArrayList(); stringList.add("One flew over the cuckoo"s nest"); stringList.add("To kill a muckingbird"); stringList.add("Gone with the wind"); Stream stream = stringList.stream(); stream.flatMap((value) -> { String[] split = value.split(" "); return Arrays.asList(split).stream(); }).forEach((value) -> System.out.println(value)); } } 复制代码
在上面的例子中,每个字符串元素被拆分成单词,变成一个 List,然后从这个 List 中获取并返回流,flatMap 方法最终会把这些流合并成一个,所以最后用流终结操作 forEach 方法,遍历并输出了每个单词。One flew over the cuckoo"s nest To kill a muckingbird Gone with the wind 复制代码distinct
distinct() 会返回一个仅包含原始流中不同元素的新 Stream 实例,任何重复的元素都将会被去掉。List stringList = new ArrayList(); stringList.add("one"); stringList.add("two"); stringList.add("three"); stringList.add("one"); Stream stream = stringList.stream(); List distinctStrings = stream .distinct() .collect(Collectors.toList()); System.out.println(distinctStrings); 复制代码
在这个例子中,元素 "one" 在一开始的流中出现了两次,原始流应用 distinct 操作生成的新流中将会丢弃掉重复的元素,只保留一个 "one" 元素。所以这个例子最后的输出是:[one, two, three] 复制代码limit
limit 操作会截断原始流,返回最多只包含给定数量个元素的新流。import java.util.ArrayList; import java.util.List; import java.util.stream.Stream; public class StreamLimitExample { public static void main(String[] args) { List stringList = new ArrayList(); stringList.add("one"); stringList.add("two"); stringList.add("three"); stringList.add("one"); Stream stream = stringList.stream(); stream.limit(2) .forEach( element -> System.out.println(element)); } } 复制代码
这个例子中,因为对原始流使用了 limit(2) 操作,所以只会返回包含两个元素的新流,随后使用 forEach 操作将它们打印了出来。程序最终将会输出:one two 复制代码peek
peek() 方法是一个以 Consumer (java.util.function.Consumer,Consumer 代表的是消费元素但不返回任何值的方法) 作为参数的中间操作,它返回的流与原始流相同。当原始流中的元素开始迭代时,会调用 peek 方法中指定的 Consumer 实现对元素进行处理。
正如 peek 操作名称的含义一样,peek() 方法的目的是查看流中的元素,而不是转换它们。跟其他中间操作的方法一样,peek() 方法不会启动流中元素的内部迭代,流需要一个终结操作才能开始内部元素的迭代。
peek() 方法在流处理的 DEBUG 上的应用甚广,比如我们可以利用 peek() 方法输出流的中间值,方便我们的调试。Stream.of("one", "two", "three","four").filter(e -> e.length() > 3) .peek(e -> System.out.println("Filtered value: " + e)) .map(String::toUpperCase) .peek(e -> System.out.println("Mapped value: " + e)) .collect(Collectors.toList()); 复制代码
上面的例子会输出以下调试信息。Filtered value: three Mapped value: THREE Filtered value: four Mapped value: FOUR 复制代码流的终结操作
Stream 的终结操作通常会返回单个值,一旦一个 Stream 实例上的终结操作被调用,流内部元素的迭代以及流处理调用链上的中间操作就会开始执行,当迭代结束后,终结操作的返回值将作为整个流处理的返回值被返回。long count = stream .map((value) -> value.toLowerCase()) .map((value) -> value.toUpperCase()) .map((value) -> value.substring(0,3)) .count(); 复制代码
Stream 的终结操作 count() 被调用后整个流处理开始执行,最后将 count() 的返回值作为结果返回,结束流操作的执行。这也是为什么把他们命名成流的终结操作的原因。
上面例子,应用的中间操作 map 对流处理的结果并没有影响,这里只是做一下演示。
下面我们把常用的流终结操作说一下。anyMatch
anyMatch() 方法以一个 Predicate (java.util.function.Predicate 接口,它代表一个接收单个参数并返回参数是否匹配的函数)作为参数,启动 Stream 的内部迭代,并将 Predicate 参数应用于每个元素。如果 Predicate 对任何元素返回了 true(表示满足匹配),则 anyMatch() 方法的结果返回 true。如果没有元素匹配 Predicate,anyMatch() 将返回 false。import java.util.ArrayList; import java.util.List; import java.util.stream.Stream; public class StreamAnyMatchExample { public static void main(String[] args) { List stringList = new ArrayList(); stringList.add("One flew over the cuckoo"s nest"); stringList.add("To kill a muckingbird"); stringList.add("Gone with the wind"); Stream stream = stringList.stream(); boolean anyMatch = stream.anyMatch((value) -> value.startsWith("One")); System.out.println(anyMatch); } } 复制代码
上面例程的运行结果是 true , 因为流中第一个元素就是以 "One" 开头的,满足 anyMatch 设置的条件。allMatch
allMatch() 方法同样以一个 Predicate 作为参数,启动 Stream 中元素的内部迭代,并将 Predicate 参数应用于每个元素。如果 Predicate 为 Stream 中的所有元素都返回 true,则 allMatch() 的返回结果为 true。如果不是所有元素都与 Predicate 匹配,则 allMatch() 方法返回 false。import java.util.ArrayList; import java.util.List; import java.util.stream.Stream; public class StreamAllMatchExample { public static void main(String[] args) { List stringList = new ArrayList(); stringList.add("One flew over the cuckoo"s nest"); stringList.add("To kill a muckingbird"); stringList.add("Gone with the wind"); Stream stream = stringList.stream(); boolean allMatch = stream.allMatch((value) -> value.startsWith("One")); System.out.println(allMatch); } } 复制代码
上面的例程我们把流上用的 anyMatch 换成了 allMatch ,结果可想而知会返回 false,因为并不是所有元素都是以 "One" 开头的。noneMatch
Match 系列里还有一个 noneMatch 方法,顾名思义,如果流中的所有元素都与作为 noneMatch 方法参数的 Predicate 不匹配,则方法会返回 true,否则返回 false。import java.util.ArrayList; import java.util.List; import java.util.stream.Stream; public class StreamNoneExample { public static void main(String[] args) { List stringList = new ArrayList<>(); stringList.add("abc"); stringList.add("def"); Stream stream = stringList.stream(); boolean noneMatch = stream.noneMatch((element) -> { return "xyz".equals(element); }); System.out.println("noneMatch = " + noneMatch); //输出 noneMatch = true } } 复制代码collect
collect() 方法被调用后,会启动元素的内部迭代,并将流中的元素收集到集合或对象中。import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; import java.util.stream.Stream; public class StreamCollectExample { public static void main(String[] args) { List stringList = new ArrayList<>(); stringList.add("One flew over the cuckoo"s nest"); stringList.add("To kill a muckingbird"); stringList.add("Gone with the wind"); Stream stream = stringList.stream(); List stringsAsUppercaseList = stream .map(value -> value.toUpperCase()) .collect(Collectors.toList()); System.out.println(stringsAsUppercaseList); } } 复制代码
collect() 方法将收集器 -- Collector (java.util.stream.Collector) 作为参数。在上面的示例中,使用的是 Collectors.toList() 返回的 Collector 实现。这个收集器把流中的所有元素收集到一个 List 中去。count
count() 方法调用后,会启动 Stream 中元素的迭代,并对元素进行计数。import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.stream.Stream; public class StreamExamples { public static void main(String[] args) { List stringList = new ArrayList(); stringList.add("One flew over the cuckoo"s nest"); stringList.add("To kill a muckingbird"); stringList.add("Gone with the wind"); Stream stream = stringList.stream(); long count = stream.flatMap((value) -> { String[] split = value.split(" "); return Arrays.asList(split).stream(); }).count(); System.out.println("count = " + count); // count = 14 } } 复制代码
上面的例程中,首先创建一个字符串 List ,然后获取该 List 的 Stream,为其添加了 flatMap() 和 count() 操作。 count() 方法调用后,流处理将开始迭代 Stream 中的元素,处理过程中字符串元素在 flatMap() 操作中被拆分为单词、合并成一个由单词组成的 Stream,然后在 count() 中进行计数。所以最终打印出的结果是 count = 14。findAny
findAny() 方法可以从 Stream 中找到单个元素。找到的元素可以来自 Stream 中的任何位置。且它不提供从流中的哪个位置获取元素的保证。import java.util.ArrayList; import java.util.List; import java.util.Optional; import java.util.stream.Stream; public class StreamFindAnyExample { public static void main(String[] args) { List stringList = new ArrayList<>(); stringList.add("one"); stringList.add("two"); stringList.add("three"); stringList.add("one"); Stream stream = stringList.stream(); Optional anyElement = stream.findAny(); if (anyElement.isPresent()) { System.out.println(anyElement.get()); } else { System.out.println("not found"); } } } 复制代码
findAny() 方法会返回一个 Optional,意味着 Stream 可能为空,因此没有返回任何元素。我们可以通过 Optional 的 isPresent() 方法检查是否找到了元素。
Optional 类是一个可以为 null 的容器对象。如果值存在则 isPresent() 方法会返回true,调用get()方法会返回容器中的对象,否则抛出异常:NoSuchElementExceptionfindFirst
findFirst() 方法将查找 Stream 中的第一个元素,跟 findAny() 方法一样,也是返回一个 Optional,我们可以通过 Optional 的 isPresent() 方法检查是否找到了元素。import java.util.ArrayList; import java.util.List; import java.util.Optional; import java.util.stream.Stream; public class StreamFindFirstExample { public static void main(String[] args) { List stringList = new ArrayList<>(); stringList.add("one"); stringList.add("two"); stringList.add("three"); stringList.add("one"); Stream stream = stringList.stream(); Optional anyElement = stream.findFirst(); if (anyElement.isPresent()) { System.out.println(anyElement.get()); } else { System.out.println("not found"); } } } 复制代码forEach
forEach() 方法我们在介绍 Collection 的迭代时介绍过,当时主要是拿它来迭代 List 的元素。它会启动 Stream 中元素的内部迭代,并将 Consumer (java.util.function.Consumer, 一个函数式接口,上面介绍过) 应用于 Stream 中的每个元素。 注意 forEach() 方法的返回值是 void。import java.util.ArrayList; import java.util.List; import java.util.stream.Stream; public class StreamExamples { public static void main(String[] args) { List stringList = new ArrayList(); stringList.add("one"); stringList.add("two"); stringList.add("three"); Stream stream = stringList.stream(); stream.forEach(System.out::println); } } 复制代码
注意,上面例程中 forEach 的参数我们直接用了Lambda 表达式引用方法的简写形式。min
min() 方法返回 Stream 中的最小元素。哪个元素最小是由传递给 min() 方法的 Comparator 接口实现来确定的。import java.util.ArrayList; import java.util.List; import java.util.Optional; import java.util.stream.Stream; public class StreamMinExample { public static void main(String[] args) { List stringList = new ArrayList<>(); stringList.add("abc"); stringList.add("def"); Stream stream = stringList.stream(); // 作为 min 方法参数的Lambda 表达式可以简写成 String::compareTo // Optional min = stream.min(String::compareTo); Optional min = stream.min((val1, val2) -> { return val1.compareTo(val2); }); String minString = min.get(); System.out.println(minString); // abc } } 复制代码
min() 方法返回的是一个 Optional ,也就是它可能不包含结果。如果为空,直接调用 Optional 的 get() 方法将抛出 异常--NoSuchElementException。比如我们把上面的 List 添加元素的两行代码注释掉后,运行程序就会报Exception in thread "main" java.util.NoSuchElementException: No value present at java.util.Optional.get(Optional.java:135) at com.example.StreamMinExample.main(StreamMinExample.java:21) 复制代码
所以最好先用 Optional 的 ifPresent() 判断一下是否包含结果,再调用 get() 获取结果。max
与 min() 方法相对应,max() 方法会返回 Stream 中的最大元素,max() 方法的参数和返回值跟 min() 方法的也都一样,这里就不再过多阐述了,只需要把上面求最小值的方法替换成求最大值的方法 max() 即可。Optional min = stream.max(String::compareTo); 复制代码reduce
reduce() 方法,是 Stream 的一个聚合方法,它可以把一个 Stream 的所有元素按照聚合函数聚合成一个结果。reduce()方法接收一个函数式接口 BinaryOperator 的实现,它定义的一个apply()方法,负责把上次累加的结果和本次的元素进行运算,并返回累加的结果。import java.util.ArrayList; import java.util.List; import java.util.Optional; import java.util.stream.Stream; public class StreamReduceExample { public static void main(String[] args) { List stringList = new ArrayList<>(); stringList.add("One flew over the cuckoo"s nest"); stringList.add("To kill a muckingbird"); stringList.add("Gone with the wind"); Stream stream = stringList.stream(); Optional reduced = stream.reduce((value, combinedValue) -> combinedValue + " + " + value); // 写程序的时候记得别忘了 reduced.ifPresent() 检查结果里是否有值 System.out.println(reduced.get()); } } 复制代码
reduce() 方法的返回值同样是一个 Optional 类的对象,所以在获取值前别忘了使用 ifPresent() 进行检查。
streadm 实现了多个版本的reduce() 方法,还有可以直接返回元素类型的版本,比如使用 reduce 实现整型Stream的元素的求和import java.util.ArrayList; import java.util.List; public class IntegerStreamReduceSum { public static void main(String[] args) { List intList = new ArrayList<>(); intList.add(10); intList.add(9); intList.add(8); intList.add(7); Integer sum = intList.stream().reduce(0, Integer::sum); System.out.printf("List 求和,总和为%s ", sum); } } 复制代码
toArray
toArray() 方法是一个流的终结操作,它会启动流中元素的内部迭代,并返回一个包含所有元素的 Object 数组。List stringList = new ArrayList(); stringList.add("One flew over the cuckoo"s nest"); stringList.add("To kill a muckingbird"); stringList.add("Gone with the wind"); Stream stream = stringList.stream(); Object[] objects = stream.toArray(); 复制代码
不过 toArray 还有一个重载方法,允许传入指定类型数组的构造方法,比如我们用 toArray 把流中的元素收集到字符串数组中,可以这么写:String[] strArray = stream.toArray(String[]::new); 复制代码流的拼接
Java 的Stream 接口包含一个名为 concat() 的静态方法,它可以将两个流连接成一个。import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; import java.util.stream.Stream; public class StreamConcatExample { public static void main(String[] args) { List stringList = new ArrayList<>(); stringList.add("One flew over the cuckoo"s nest"); stringList.add("To kill a muckingbird"); stringList.add("Gone with the wind"); Stream stream1 = stringList.stream(); List stringList2 = new ArrayList<>(); stringList2.add("Lord of the Rings"); stringList2.add("Planet of the Rats"); stringList2.add("Phantom Menace"); Stream stream2 = stringList2.stream(); Stream concatStream = Stream.concat(stream1, stream2); List stringsAsUppercaseList = concatStream .collect(Collectors.toList()); System.out.println(stringsAsUppercaseList); } } 复制代码从数组创建流
上面关于 Stream 的例子我们都是从 Collection 实例的 stream() 方法获取的集合包含的所有元素的流,除了这种方法之外,Java 的 Stream 接口中提供了一个名为 of 的静态方法,能支持从单个,多个对象或者数组对象快速创建流。import java.util.stream.Stream; public class StreamExamples { public static void main(String[] args) { Stream stream1 = Stream.of("one", "two", "three"); Stream stream2 = Stream.of(new String[]{"one", "two"}); System.out.println(stream1.count()); // 输出3 System.out.println(stream2.count()); // 输出2 } } 复制代码总结
上面我们把 Stream 的两大类操作:流的中间操作、流的终结操作都有哪些方法给大家列举了一遍,让大家对 Stream 能完成的操作有了大致的印象。不过为了讲解这些操作用的都是非常简单的例子,流操作的数据也都是简单类型的,主要的目的是让大家能更快速地理解 Stream 的各种操作应用在数据上后,都有什么效果。
作者:kevinyan
链接:https://juejin.cn/post/7156055319490232327
万万没想到,猪肉放进糯米里,出锅这么好吃,年夜饭必备,太香了猪肉是我们日常生活中经常食用的肉类之一,它的价格实惠营养高,深受老百姓的喜爱。猪肉蛋白质跟牛肉鸡肉一样都是优质蛋白,但是猪肉的脂肪含量很高,一些贪吃的朋友常年胡吃海塞对身体是有危害
贵州又一怪现象不喝本地名酒茅台,反而钟爱这4款贵州又一怪现象不喝本地名酒茅台,反而钟爱这4款贵州省因其独特的地理气候,出产的酱香型白酒品质最佳,所以也被誉为酱酒之乡。根据史料记载,贵州当地人在3000年前就掌握了酿酒技术。19
可乐鸡翅原来这么简单,别再买着吃了,教你在家做,比买的还香生活没有彩排,美食没有美颜。大家好,今天跟大家分享一道可乐鸡翅好吃的做法。鸡翅,我们都很熟悉,那可乐鸡翅很多人都是经常在家给家人做着吃,但是也有不少人做出来的不好吃。接下来我们就给
海南省政府批复同意一批风景名胜区总体规划海南日报讯(记者孙慧)近日,省政府同意批复我省七仙岭南丽湖临高角木色湖等一批风景名胜区总体规划。据了解,七仙岭风景名胜区总体规划(20212030年)规划风景名胜区总面积为56。8
食客们请注意今秋第一网阳澄湖大闸蟹开捕啦蟹船扬帆,横穿湖面,浪花激扬返程收网,一只只大闸蟹在网中张牙舞爪随着第一篓蟹出水,今秋的阳澄湖大闸蟹正式开捕。昨天,2022中国苏州阳澄湖大闸蟹开捕节暨昆山巴城蟹文化旅游节在苏州昆
云南石头城108户人家住一块巨石上,至今保留着千百年前的风俗在丽江城北110公里的金沙江峡谷中,有着一座真正的天险之城宝山石头城!三面皆是悬崖峭壁,一面石坡直插金沙江,百余户人家聚居在一座独立的蘑菇状巨石之上。(图源微博让鱼飞翔的水,侵删)
陇西县旅游景点陇西县旅游景点陇西县历史文化悠久,别名李氏故里中国药都。陇西因陇山以西而得名,自古为四塞之国,兵家必争之地,隶属甘肃省定西市。让我们一同走进陇西,感受西北风吹过的荒凉和厚土的热爱。
黑龙江唯一的全国十大工业旅游城市不是哈尔滨,知道是哪吗?工业旅游是近年来新兴的旅游项目。是伴随着人们对旅游资源理解的拓展而产生的旅游新概念和新产品。工业旅游最早起源于英国,迄今在欧洲北美日本等国家已颇为成熟。在我国是近几年开始发展起来的
兴庆公园的大象滑梯相信您能点开这篇文章,小时候一定在兴庆公园的大象母子滑梯上溜下来过,如果您小时候去过兴庆公园,玩过大象滑梯,请您点个赞,看看有多少玩过大象滑梯。大象母子滑梯建于1964年的大象滑梯
欧游记(26)有镇馆三宝的卢浮宫6月23日。上午1030参观卢浮宫。卢浮宫坐落在塞纳河中段的北岸,是世界三大博物馆之一。始建于1204年,历经800多年扩建重修才达到今天的规模。卢浮宫是法国最大的王宫建筑之一,它
汉中西乡县的红崖洞虽然不大,但它包含的历史典故倒是很有来头在汉中西乡县城东泾洋河畔,有一处道观古迹。道观名为红崖洞,停车进门观之,很有一些历史韵味。红崖洞胜迹碑载红崖洞在西乡县城东15里,临泾洋河东岸峭壁之上,始建于明代,清顺七年(公元一