信息时代,大数据成为一切操作的基石,即使是人工智能及深度学习相关领域也离不开数据的支撑。因此文本挖掘、图像处理等基础技术愈加重要,本文将着重介绍文本挖掘中的加权算法——TF-idf及其Java实现。
TF-idf常被作为一种统计算法对分词结果进行加权排序,以得出文本中重要的一些词汇,因此可以视作分类算法或评级算法。显然从其名字我们可以轻松地知道,该算法由两部分组成,TF及idf。
TF(Term Frequency,词频)指的是在一段文本中,某一词汇出现的频率。idf(inverse document frequency,逆文本频率)指的是许多文件中某一词汇的重要程度。你可以前往维基中国查看更多详细的内容。
网上不乏该算法的实现,但均乏善可陈且本人钟情于Java,故开发了Java实现。
作者采用优秀的国产开源分词软件HanLP进行分词,对它有兴趣的同学可自行前往其Github主页学习。1
NotionalTokenizer.segment(corpusText.toString());
1 | /** |
1 | ** |
我将使用纳兰性德的190+首诗词作品作为语料来源,并将其TF值及TF-idf结果分别输出。如果你需要类似的诗词语料,可以考虑使用我最近的开源工具进行爬取。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34public class TF_IDFUtilsTests {
private List<String> fileList;
@Before
public void initialDate() throws IOException {
String route = Resources.getResource("text_mining/poems").getPath(), result = "";
List<File> files = FilesUtil.getAllFiles(route, 2);
fileList = new ArrayList<>();
for (File file : files) {
fileList.add(FileUtils.readFileToString(file, "UTF-8"));
}
}
@Test
public void countTF_IDF() {
// HanLP.Config.enableDebug();
String result = TF_IDFUtils.countIDF(fileList, true);
System.out.println(result);
}
@Test
public void countTF() {
StringBuilder result = new StringBuilder();
Map<String, Integer> tfMap = TF_IDFUtils.countTFByText(fileList, true);
List<Map.Entry<String, Integer>> tfArrayList = new ArrayList<>(tfMap.entrySet());
tfArrayList.sort(Comparator.comparing(Map.Entry::getValue));
result.append("--------------TF Result--------------\n");
for (Map.Entry<String, Integer> entry : tfArrayList) {
result.append(entry.getKey()).append("\t").append(entry.getValue()).append("\n");
}
System.out.println(result.toString());
}
}
结果展示:
本教程志在细致入微、深入底层,你将体验从Stream的创建开始(creation)到并行执行(parallel execution)的完整过程,以此体会Stream API的实际用处。
为了理解下面的文章,读者需要掌握Java 7基础知识(Lambda表达式、Optional、方法引用)以及熟悉Stream API,如果你并不熟悉它们甚至一无所知,建议你先阅读我们之前的文章-Java8 新特性 以及 Java 8 Streams 介绍。
创建一个Stream实例有多种方式,每种创建方式对应Stream的一个来源。但单个Stream实例每次创建之后,其来源将无法修改,这意味着Stream实例具备源头不可变性,不过我们却可以从单个源创建多个Stream实例。
方法empty()被用于创建一个Empty Stream:1
Stream<String> streamEmpty = Stream.empty;
上述代码段创建的Empty Stream通常被用于避免null对象或零元素对象的streams(streams with no element)返回结果为null:1
2
3public Stream<String> streamOf(List<String> list){
return lsit == null || list.isEmpty() ? Stream.empty() : list.streams();
}
我们可以创建任意Collection接口衍生类(Collection->List、Set、Queue)的Streams:1
2Collections<String> collection = Arrays.asList("a", "b", "c");
Stream<Stirng> streamOfCollection = collection.stream();
接下来的这段代码展示的是数组Stream:1
Stream<String> streamOfArray = Stream.of("a", "b", "c");
当然我们可以先创建熟悉的数组类型,再以它为源创建Stream,而且我们可以选择Stream中包含的元素数量:1
2
3String[] arr = new String[]{"a", "b", "c"};
Stream<String> streamOfArrayFull = Arrays.stream(arr);
Stream<String> streamOfArrayPart = Arrays.stream(arr, 1, 3);
当builder被用于指定参数类型时,应被额外标识在声明右侧,否则方法build()将创建一个Stream(Object)实例:1
Stream<String> streamBuilder = Stream.<String>builder().add("a").add("b").add("c").build();
方法generator()接受一个供应器Supplier1
Stream<String> streamOfGenerated = Stream.generate( () -> "element").limit(10);
上述代码将创建十个内容为“element”的生成流。
另一种创建无限流的方法是通过调用方法iterate(),同样的它也需要使用方法limit()对目标流的元素大小进行限制:1
Stream<Integer> streamItreated = Stream.iterate(40, n -> n + 2).limit(20);
迭代流即采用迭代的方法作为元素生产方式,类似于高中数学中的f(x),f(f(x)),etc。上述例子中,生成流的第一个元素是迭代器iterate()中的第一个元素40,从第二个元素开始的每个新元素都与上个元素有关,在此例中,生成流中的元素为:40、42、44、…78、80。
Java8提供了创建三大基础数据类型(int、long、double)stream的方式。由于Stream
使用它们能够避免不必要的自动装箱1以提高生产效率。1
2IntStream intStream = IntStream.range(1, 3);
LongStream longStream = LongStream.rangeClosed(1, 3);
方法range(int startInclusive, int endInclusive)创建了一个有序流(从startInclusive到endInclusive)。它使后面的值每个增加1,但却不包括最后一个参数,即此方法的结果是具备上限的。方法rangeClosed(int startInclusive, int endInclusive)与range()大致相同,但它却包含了最后一个值。
这两个方法用于生成三大基本数据类型的stream。
此外,Java8之后,类Random也提供了拓展方法用于生成基础数据类型的stream。例如,下述代码创建了一个含有三个随机值的DoubleStream:1
2Random random = new Random();
DoubleStream doubleStream = random.doubles(3);
String类型也可以作为生成stream的源,这得益于方法chars()的帮助,此外由于JDK中没有CharStream接口,IntStream也被用来表示字符流(stream of chars)1
IntStream streamOfChars = "abc".chars();
下例中通过特征的正则表达式将一个字符串割裂成(break into)其子串。1
2Stream<String> streamOfString =
Pattern.compile(", ").spitAsStream("a", "b", "c");
Java NIO2类文件允许通过方法lines()生成文本文件的Stream1
2
3Path path = Paths.get("C:\\file.txt");
Stream<String> streamOfString = Files.lines(path);
Stream<String> streamWithCharset = Files.lines(path, Charset.forName("utf-8"));
ps:在方法lines()中也可以通过Charset设置文件编码。
只要调用生成操作(中间操作)就会实例化一个stream并生成一个可获取的引用,但执行终端操作会使得stream无法访问。为了证明这一点,我们不妨先忘记它,毕竟实践是检验真理的唯一标准。
以下代码如果不考虑冗长的话将是有效的:1
2Stream<String> stream = Stream.of("a", "b", "c").filter(element -> element.contains("b"));
Optional<String> anyElement = stream.findAny();
但是倘若我们在执行终端操作后重新使用相同的引用,则会不可避免的触发IllegalStateException。1
Optional<String> firstElement = stream.findFirst();
IllegalStateException是一个运行时异常(RuntimeException),即编译器将不会提示此错误。因此必须记得,JAVA8 不允许重复使用stream
这一设计是合乎逻辑的,因为stream从设计上旨在提供一个将有限操作(指函数体中元素的相关操作)的序列,而不是存储元素。
因此想让以前的代码正常工作我们得先改一改:1
2
3
4
5List<String> elements =
Stream.of("a", "b", "c").filter(element -> element.contains("b"))
.collect(Collectors.toList());
Optional<String> anyElement = elements.stream().findAny();
Optional<String> firstElement = elements.stream().findFirst();
想要执行源数据集的操作集并聚合它们,你需要以下三个部分——源(Source)、中间操作(Intermediate operations)和终结操作(terminal operation)。
中间操作返回的是一个新的可操作stream。举个例子,为了在一个包含少量元素Stream的基础之上新建Stream,我们可以调用方法skip():1
Stream<String> oneModifiedStream = Stream.of("abcd", "bbcd", "cbcd").skip(1);
如果需要多次修改,则可以采用多次中间操作。假如我们还需要将Stream1
Stream<String> twiceModifiedStream = stream.skip(1).map(element -> element.subString(0, 3));
正如你所见,上例中map()使用Lambda表达式作为其参数对stream中的各元素进行处理。
stream本身是毫无价值的,编程人员最感兴趣的其实是终结操作(terminal operation),它可以是一个元素也可以是一个行为。只有在终结操作里才能对每个stream进行使用。正确的且最方便的stream操作方式就是Stream Pipeline,即stream源->中间操作->终结操作。如例:1
2
3List<String> list = Arrays.asList("abc1", "abc2", "abc3");
long size = list.stream().skip(1)
.map(element -> element.substring(0, 3)).sorted().count();
中间操作是懒式调用的,这意味着只有在终结操作需要它们的时候中间操作才会被唤醒。
为了证明这个事实,假象我们有个方法wasCalled(),每当它被唤醒时使内部变量counter自增。1
2
3
4private long counter;
private void wasCalled() {
counter++;
}
接下来让我们在filter()操作中唤起wasCalled():1
2
3
4
5
6List<String> list = Arrays.asList(“abc1”, “abc2”, “abc3”);
counter = 0;
Stream<String> stream = list.stream().filter(element -> {
wasCalled();
return element.contains("2");
});
由于有三个变量,想象中filter()中的代码块将被执行三次,wasCalled()执行三次之后counter的值应为3,但是执行之后counter并未发生改变,仍然为0,也就是说filter()一次也没有被唤醒,这个原因就是缺失了终结操作(terminal operation)。
那接下来我们不妨再上述代码的基础之上添加一次map()操作和一个终结操作——findFirst(),并采用打日志的方式帮助我们了解方法调用时机及顺序。1
2
3
4
5
6
7Optional<String> stream = list.stream().filter( element -> {
log.info("filter() was called!");
return element.contains("2");
}).map(element -> {
log.info("map() was called!");
return element.toUpperCase();
}).findFirst();
日志结果显示filter()被唤醒了两次,而map()仅仅被调用一次,这是由于管道流是垂直执行的。在此例中第一个元素不满足filter()的要求,因此filter()被调用第二次以查找合适的结果,通过之后即进行map()操作,此时就没有第三次机会执行filter()操作了。findFirst()就能找出源数据集中第一个含有“2”的字符串的全大写字符串了。因此,懒调用使得不必相继调用两个中间操作(filter()和map())才能完成任务了。
从性能的角度考虑,正确的执行顺序是采用上文提到的流式管道(Stream Pipeline):1
2
3
4long size = list.stream().map(element -> {
wasCalled();
return element.substring(0, 3);
}).skip(2).count();
执行这段代码将使counter自增长3次,这意味着stream的方法map()将被调用3次,但最终size的值为1。这意味着结果流(resulting stream)中仅仅只有一个元素,毫无疑问在三次消息处理中程序跳过了两次处理。
如果我们改变skip()和map()的执行顺序,counter将只自增长一次。也即是map()只被调用一次:1
2
3
4long size = list.stream().skip(2).map(element -> {
wasCalled();
return element.substring(0, 3);
}).count();
以上示例告诉我们一个规则:用于减少流中元素数量的中间操作,应当放置在处理操作之前。因此,保证在你的Stream Pipeline规则中按照这样的顺序编码:skip() –> filter() –> distinct()
API提供了大量的终端操作用以聚合一个stream为一种数据类型或变量。比如:count()、max()、min()、sum(),但是这些方法都是预定义的。但如果用户需要自定义一个stream的聚合操作呢?官方提供了两个方法用以实现此类需求:reduce() 和 collect()。
此方法提供了三种变种,不同之处是它们的签名以及返回类型。reduce()方法具有下列参数:
identify(标识器) - 累积器的初始值或当stream为空时的默认值。
accumulator(累积器) - 提供设定聚合元素之逻辑的功能,每次规约(reducing)累积器都会创建一个新的值,新值的大小等于stream的大小,并且只有上一个值是可用的。这非常有助于提升性能。
combiner(组合器) - 提供聚合accumulator(累积器)中元素的功能,combiner是唯一一个能从不同线程以并行模式聚合累积器中结果的方法。
好,让我们来实战一下吧:1
2OptionalInt reduced =
IntStream.range(1, 4).reduce((a, b) -> a + b);
reduced = 6 = 1 + 2 + 3。1
2int reducedTwoParams =
IntStream.range(1, 4).reduce(10, (a, b) -> a + b);
reducedTwoParams = 16 = 10 + 1 + 2 + 3。1
2
3
4
5int reducedParams = Stream.of(1, 2, 3)
.reduce(10, (a, b) -> a + b, (a, b) -> {
log.info("combiner was called");
return a + b;
});
这一结果与上文中的16一样,并且不会打出日志,因为combiner没有被唤起。为了唤醒combiner,stream应当是并行的:1
2
3
4
5int reducedParallel = Arrays.asList(1, 2, 3).parallelStream()
.reduce(10, (a, b) -> a + b, (a, b) -> {
log.info("combiner was called");
return a + b;
});
此时,结果变为36,并且combiner被唤起了两次。规约(reduce)运转的算法为:每当stream中的元素通过identify(标识器)时accumulator(累积器)均被调用,最终累积器调用了3次。上述行为是并行完成的,因此造成了(10+1=11; 10+2=12; 10+3=13;)。最终combiner(组合器)混合了三次的结果,通过两次迭代完成运算(12+13=25; 25+11=36;)。
stream的规约也可以被其他的终结方法执行——collect()。它接收了一个名为collector的参数,此参数注明规约的流程。官方已经创建了预定义的收集器,我们可以在这些收集器的帮助下访问它们。
下面我们将看到使用List作为所有stream的来源:1
2
3List<Product> productList = Arrays.asList(new Product(23, "potatoes"),
new Product(14, "orange"), new Product(13, "lemon"),
new Product(23, "bread"), new Product(13, "sugar"));
转换一个stream为Collection集合(Collection、List、Set、Queue、etc)。1
2List<String> collectorCollection =
productList.stream().map(Product::getName).collect(Collectors.toList());
规约为String类型:1
2String listToString = productList.stream().map(Product::getName)
.collect(Collectors.joining(", ", "[", "]"));
join()方法拥有三个参数(delimiter, prefix, suffix),使用join()最便捷之处在于程序员不需要考虑stream的起始与结束甚至界定符,Collector会考虑到这些的。
计算stream中所有数字元素的平均值1
2double averagePrice = productList.stream()
.collect(Collectors.averagingInt(Product::getPrice));
计算stream中所有数字元素的和1
2int summingPrice = productList.stream()
.collect(Collectors.summingInt(Product::getPrice));
方法averagingXX()、summingXX()和summarizingXX()适用于基础数据类型(int,long,double),也适用于它们的封装类( Integer,Long,Double)。一个很有效的功能技术提供映射,因此开发者也不是一定需要在collect()方法之后使用map()操作才能完成映射的。
收集stream元素集的统计信息:1
2IntSummaryStatistics statistics = productList.stream()
.collect(Collectors.summarizingInt(Product::getPrice));
通过使用IntSummaryStatistics的生成实例,开发者能够通过请求toString()方法创建一个统计报告,结果将是一系列显而易见的结果:IntSummaryStatistics{count=5, sum=86, min=13, average=17,200000, max=23}。通过调用上述方法getCount()、getSum()、getMin()、getAverage()、getMax(),我们也很容易从对象中提取出count、sum、min、average的值,这是因为所有的值均可以从单个管道中获取。
采用指定方法组合stream中的元素:1
2Map<Integer, List<Product>> collectorMapOfLists = productList.stream()
.collect(Collectors.groupingBy(Product::getPrice));
此例中stream将根据group规则将所有元素规约成一个map。
根据一些描述对stream进行分组:1
2
3Set<Product> unmodifiableSet = productList.stream()
.collect(Collectors.collectingAndThen(Collectors.toSet(),
Collections::unmodifiableSet));
这种相对特殊的情况里,collection将stream转化为一个Set,之后在此基础上创建了一个不可变的Set。
Custome collector(自定义收集器):
假若我们因为一些特定的原因需要创建自定义的收集器,那更简介轻快的方法是采用Collection的of()方法:1
2
3
4
5
6
7
8
9Collector<Product, ?, LinkedList<Product>> toLinkedList =
Collector.of(LinkedList::new, LinkedList::add,
(first, second) -> {
first.addAll(second);
return first;
});
LinkedList<Product> linkedListOfPersons =
productList.stream().collect(toLinkedList);
在上例中,Collection的实例被规约成了一个LinkedList
在Java8之前,并行化十分复杂。ExecutorService和FornJoin的出现大大降低了并行开发的复杂度,但它们都无不避免的关注在如何创建一个特征鲜明的executor,以及如何去运行它等等。Java8提倡了一种新的方式用于在函数类型中实现并行化。
API提供并行流用以并行化执行操作。当stream的源是一个数组或者Collection时,在parallelStream()方法的帮助下可以实现并行化:1
2
3
4
5Stream<Product> streamOfCollection = productList.parallelStream();
boolean isParallel = streamOfCollection.isParallel();
boolean bigPrice = streamOfCollection
.map(product -> product.getPrice() * 12)
.anyMatch(price -> price > 200);
但如果stream的源不是数组或者集合类型时,parallel()方法就应该被使用了:1
2IntStream intStreamParallel = IntStream.range(1, 150).parallel();
boolean isParallel = intStreamParallel.isParallel();
上例中,Stream API自动使用了ForkJoin框架去完成并行操作。默认情况下,公共线程池将被使用,不会(至少暂时不会)给它单独分配线程。当stream处于并行状态时,应当注意可能产生阻塞的操作,当对时间效率有所追求且操作可并行时应当转换为并行stream(理由是假如某个任务大小远远多于其他任务,那它将更加耗时)。当然啦,并行模式也可以转换回串行模式,只要使用sequential()方法就能做到这点:1
2IntStream intStreamSequential = intStreamParallel.sequential();
boolean isParallel = intStreamSequential.isParallel();
Stream API在对链式数据进行操作时体现了其强大性,但也易于理解。它通过引用的方法规约大容量的数据,构建了更健壮的程序,最主要的是提升了项目开发的生产力。
在本文中stream均是未被关闭的(我们没有调用close()方法或者其他的终结操作),但在实际项目中,不要这样无节制的放纵stream的存在,这将逐步耗尽你的内存,造成内存泄漏程序崩溃的风险。
最后,本文所对应的示例代码你可以在github-core-java-8上获取到。祝福你身体健康,编码顺利!
Lambda(λ,希腊数字中的第十一个数字,由于λ演算式的存在,它也代表闭包)表达式是JAVA 8中最令人激动的新特性,它使得Java编程中出现了函数式编程的概念,在其他高级语言中如Python(解释型、动态数据类型、面向对象),常用Lambda表达式创建匿名函数1。Lambda表达式允许我们将函数当成一个参数看待,可以将其传递给一个方法或者直接将表达式所在代码块作为数据处理,这一设计在JAVA 8之前只能采用匿名内部类2的方式实现,这损耗了大量的编程时长及读码效率。
Example 1 遍历数组
Java 71
2
3
4Integer[] arr = new Integer[]{2, 9, -2, 3};
for(int i : arr){
System.out.println(i);
}
Java 81
2
3Arrays.asList(2, 9, -2, 3).foreach( (Integer e) -> {
System.out.println(e);
} );
可以看到最简单的Lambda表达式可以由数值列表,(变量代表名),->以及{行为代码}组成。但实际上,()包括变量e的类型名都是可以省略的,省略的变量类型名由编译器自行推理,深知{}也是可以省略的,所以最短代码应该是:1
Arrays.adList(2, 9, -2, 3).foreach( e -> System.out.println(e) );
Lambda表达式可以引用类成员变量或全局变量,但JVM会自动将其隐形转换成final类型,理由与匿名内部类的参数引用时必须为final一致3。1
2String separator = ",";
Arrays.adList(2, 9, -2, 3).foreach( e -> System.out.print(e) + separator ); // 变量separator将被隐式转换为final类型。
为了使Java中原有的功能能够与Lambda表达式结合使用,官方规定函数接口(除下文即将介绍的默认函数及静态函数外,只有一个函数的接口)能够隐式转换成Lambda表达式,java.lang.Runnable和java.concurrent.Callable是函数接口的最佳例子。此外,为了解决函数接口定义与Lambda表达式的冲突,官方提供了一个特殊的注解@FuntionalInterface用以表示函数接口,这意味着你以后定义上述函数接口将采用此注解标识,在Jdk中所有相关的函数也已经加上此注解。如:1
2
3
4@FunctionalInterface
public interface Callable<V> {
V call() throws Exception;
}
众所周知,接口相关知识点是面试中老生常谈的话题。在以往接口被定义为抽象方法的集合,接口中的方法会被隐式指定为public abstract,而变量会被隐式指定为public static final,其他修饰符会导致报错。而现在,Java中接口的定义将被修改,接口中除public抽象函数外新增了默认方法和静态方法。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21public interface Defaulable {
default void hello(){
System.out.println("这是接口的默认方法");
}
static void create(){
System.out.println("这是接口的静态方法");
}
}
public class DefauleImp implements Defaulable{
// @Override
// public void hello() {
// System.out.println("Hello world");
// }
public static void main(String[] args) {
new DefauleImp().hello();
Defaulable.create();
}
}
默认方法可被继承或重写,而静态方法可与类的静态方法一样直接通过接口名.方法名调用。
方法引用的最大用途是简写Lambda表达式。
方法引用 | Lambda表达式 |
---|---|
String::valueOf | x -> String.valueOf(x) |
Object::toString | x -> x.toString() |
x::toString | () -> x.toString() |
ArrayList::new | () -> new ArrayList<>() |
犹记得在旧版的Java教程中没有注解的出现,当学习Struts等第三方流行框架时出现注解使我极其不适。而现在注解已经成为JAVA世界一种独特且无可替代的定义方式。JAVA8中,注解几乎可以用在任何元素之上:类、接口、元素、方法,甚至是异常。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.ArrayList;
import java.util.Collection;
public class Annotations {
@Retention( RetentionPolicy.RUNTIME )
@Target( { ElementType.TYPE_USE, ElementType.TYPE_PARAMETER } )
public @interface NonEmpty {
}
public static class Holder< @NonEmpty T > extends @NonEmpty Object {
public void method() throws @NonEmpty Exception {
}
}
@SuppressWarnings( "unused" )
public static void main(String[] args) {
final Holder< String > holder = new @NonEmpty Holder< String >();
@NonEmpty Collection< @NonEmpty String > strings = new ArrayList<>();
}
}
在Java8以前,需要在运行时得到参数的名称是一件比较困难的事情,程序员们虽然提供了诸如Paranamer liberary等方法,但用起来总归不算顺畅,而现在Java8从字节码层面(使用新的javac编译器以及-parameters参数)和语言层面(Parameter.getName()和反射API)提供了这一支持。1
2
3
4
5
6
7
8
9
10public static void main(String[] args) {
for (Method m : ${ClassName}.class.getMethods()) {
System.out.println("----------------------------------------");
System.out.println(" method: " + m.getName());
System.out.println(" return: " + m.getReturnType().getName());
for (Parameter p : m.getParameters()) {
System.out.println("parameter: " + p.getType().getName() + ", " + p.getName());
}
}
}
但是在JAVA8中这个功能是默认关闭的,如果需要打开需要加上参数-parameters进行编译,如果你使用maven作为构建工具,也可以直接在编译插件中加入此参数,如下:1
2
3
4
5
6
7
8
9
10<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<compilerArgument>-parameters</compilerArgument>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
Java中经常会出现NullException,为了检验空值异常,程序员经常需要添加许多与业务逻辑无关的检测代码,这既破坏了代码美感,也耗费了宝贵的开发时间,因此Java8中仿照谷歌开源库Guava使用了Optional类,此类提供了有效的接口用于null检查,如下:1
2
3
4Optional< String > fullName = Optional.ofNullable( "null" );
System.out.println( "Full Name is set? " + fullName.isPresent() );
System.out.println( "Full Name: " + fullName.orElseGet( () -> "[none]" ) );
System.out.println( fullName.map( s -> "Hey " + s + "!" ).orElse( "Hey Stranger!" ) );
Stream也是Java中非常重要的一个特性,在Java文档中这样定义Stream:
A sequence of elements supporting sequential and parallel aggregate operations.
翻译一下,即:
显而易见,Stream的设计源于分治法,学过并行计算框架MapReduce或Fork/Join的同学更容易理解。
由于Stream所属知识篇幅较大,有兴趣的同学可以关注本人的【翻译】Java8 Stream API 教程
作为一名计算机专业的学生,我的的确确懒惰了,这个学科容不得我半分懈怠,为了自己心中所追逐的梦想,再苦再累我也不会停息。尽全力,纳百家,观方圆,酌得失,一举一动,留待岁月之后的自己独赏。
2017年03月07日 丢失了又得到了
选择这首歌的原因是刚刚失恋,这并没有什么大不了的,只是遗憾满满,同时也怕自己不经意间错过了未来的那个人。慢慢的想通,爱究竟是什么,我到底爱她什么,怎么样维持一段爱情。然,即使我现在很仍旧很爱她,也须顺了她的意。不再啰嗦,若有缘,终相见。
都说失恋会让人成熟,的却如此,感谢500多天里你让我感受到的幸福以及你心底里珍贵的东西。如果要我选择成熟的方向,我愿意恢复2015年暑假刚刚拥有PC时的勤奋,未来很漫长,我可不能懈怠。网申了很多大公司的暑期实习,其实心里并没有底,专业知识学得不怎么好,项目有不能抬得上台面,唯一庆幸的是自己的学习能力。希望自己能够进入一家好一点的公司实习,也不枉我对父母的承诺。
失恋终会忧伤,我也不安慰自己,仿佛失去一半生命的感觉确实不好受,可我还是得立即站起来,为了梦,也为了你。
2017年03月10日 给自己打打气
最近投了很多公司的暑期实习内推,rejected了部分,心里有些失落。今天下午又同高中好友喝了酒,在我心里,这位同学是从高中开始就很厉害的人,与他喝了6瓶酒,瞬间感叹时光易逝,自己已经无法消受10瓶。一个多小时里,谈了许多事,从爱情观谈到就业观,再到家庭观、人生观,虽有些观点不尽相同,可我们那么多年一直坚持求同存异,这些事我们在饭桌上从不遮口的原因。
几盅酒过去,谈起了大一时自在而不知名的忙碌,大二我找了女朋友和他饮酒的机会便少了,又叹息再过一年将各奔前途,难再相聚。与他相比,我算是幸运地,我遇见了更多的人,但与他相比我又是悲哀的,我错过了一个特别的人。好友又与我谈起自己的专业知识,自己实习时的奇闻趣事,乐得我饶是开心。说起家人时我们两都感叹,毕业后陪家人的时间将更短,有些不忍,从没想到自己长大也会那么难受。
自己如今需要一个实习的机会,了解真正的互联网公司运作模式及开发模式,也迫切需要一份能填在简历上的东西,因此从今天开始得正式地制定一个学习计划,希望自己一直引以为豪的学习能力能为自己赢得一张暑期实习的入场券。Come On!
2017年06月25日 四月之痛
最近感触颇多。已经和最爱的人分手四个月,我向来不是犹豫不决之人,之所以一直不肯放下,是因为我认识到她在我生命之中的重要程度。故不断改正自己、不断剖析自我是我一直在我的事。正如我在知乎上所言,虽然这几年她都不可能跟我在一起了,但我真的希望我能陪她走到生命尽头。
这件事给我造成了很大的改变,我逐渐发现了我性格和行为上的缺陷,正在一一评判和解决。但我从没有放弃学习的脚步,我希望自己将情感世界与工作独立开来,我在努力做到。这四个月写了几个爬虫,完成了贴吧信息的爬取和自动登录,不过仍有bug,会持续修复。也找到了几家实习,但我不想离开长沙,因为我试过离开这座城市我有多受不了。当然,也因为我的多多宝宝,所以应该还是不会去杭州的吧。
接下来就是实习了,未来啊,等待和冲击是我不变的脚步。我会快步跑到下一个起跑点,在那里等待着我的那只兔。我会变成你最优秀的追求者,即使如今你已有新男友。我会做到不打扰,但我不会离开。我会重新生长重塑自我,但我不会抛弃我的可爱。继续coding,继续奔跑,继续等待。。
]]>
不说废话,直接放GuyHub链接:PageReplacement
1 | package cn.yodes.OS.Algorithm; |
1 | package cn.yodes.OS.Algorithm; |
1 | package cn.yodes.OS.Algorithm; |
对于参数调用问题而言,引用其实还是传值.
转自
关键字:Java 传值 传引用
这是个老生常谈的问题了,引起过无数争论,但可以说一直没有一个令人满意的回答。
有些人非要故弄玄虚,把传引用说成是栈里面放的是引用的值,说只有传值没有传引用,那看看这句经典名言吧:
O’Reilly’s Java in a Nutshell by David Flanagan (see Resources) puts it best: “Java manipulates objects ‘by reference,’ but it passes object references to methods ‘by value.’”
从这里也可以看到,David 也没那么生硬,不过是看你从哪个角度来认识这个问题,如果大家习惯c++的那种传参时的理解方式,为何不能这么比较呢?
有人已经总结过:
参见
http://www.javaresearch.org/article/3156.htm
- 对象是按引用传递的
- Java 应用程序有且仅有的一种参数传递机制,即按值传递
- 按值传递意味着当将一个参数传递给一个函数时,函数接收的是原始值的一个副本
- 按引用传递意味着当将一个参数传递给一个函数时,函数接收的是原始值的内存地址,而不是值的副本
写的没错,但是文字太多,第二条就已经把人弄糊涂了,得仔细看完4条才清楚。而且对String类型的疑惑没有解决。
这么简单的事情,何必这么绕呢?为啥没人跟c++过不去,偏要跟Java来劲?
三句话总结一下:
- 对象就是传引用
- 原始类型就是传值
- String,Integer, Double等immutable类型因为没有提供自身修改的函数,每次操作都是新生成一个对象,所以要特殊对待。可以认为是传值。
Integer 和 String 一样。保存value的类变量是Final属性,无法被修改,只能被重新赋值/生成新的对象。 当Integer 做为方法参数传递进方法内时,对其的赋值都会导致 原Integer 的引用被 指向了方法内的栈地址,失去了对原类变量地址的指向。对赋值后的Integer对象做得任何操作,都不会影响原来对象。
其他参考文章:
http://blog.darkmi.com/2010/11/28/1430.html
http://dreamhead.blogbus.com/logs/2005/05/1189478.html
http://www.javaeye.com/topic/12961
http://www.cnblogs.com/coderising/p/5697986.html
http://www.cnblogs.com/laipDIDI/articles/2524309.html
这估计是2016年我看过的最想哭的电影
七月第一次遇见安生的时候,是十三岁的时候,自此就是七年的相守。七月与安生像是两个世界的人,安生父亲早逝、母亲对她不闻不问,因此安生最不安分,是学校里的调皮孩子。而七月则恰恰相反,从小安稳平和的家庭环境使她对未来没有过怀疑。电影开始便会让人纳闷,两个完全不同性格的人为何会成为最亲密的好友呢?当然这个问题在剧末给出了很直接的答案。
七月与安生,小时候最爱的事情就是一起洗澡,这也造成剧照被网友误认为这是一部同性恋影片。七月很小的时候就穿上了文胸,而放浪形骸的安生却不愿意遭到束缚,影片的线索很多,这就算一个。安生遇见七月,有了一个新的“家”,而七月碰见安生,朦胧间找到了最真的自己。中考结束之后,七月如愿以偿的进入了重点高中,而安生却带着再也不用交作业的心思到了职校,成为了“小太妹”。因此他们也遇见了不同的男人(ps:全剧的男生仿佛都是渣男),像大多数人的青春期一样,七月喜欢同校的学霸校草苏家明,并把心事告诉了安生,安生这个时候知道自己不是七月唯一爱的人了。安生,一边在压抑一边在让步,因为他单独见了苏家明,却与苏家明互相吸引。当然,七月向家明表白了,这也为后来“真正”的七月正名。
跟七月在一起的家明却想着安生,安生仓促的离开呆了数年的城市前往北京去跟随一个流浪吉他手,只为躲避这两个最熟悉的人。在离别的时候,七月看到了安生系着的玉佛,那是家明从不离身的东西,她仿佛明白了一切,不再为七月的离开而伤感。但她没有告诉家明这一切。
转眼好多年,七月与家明进入了同一所大学念书,已经准备结婚。而安生四处漂泊,居无定所,换了四五个男朋友,她想回到她和安生的家,却不敢回去,因为那里有家明。安生每隔一段时间就会写信告诉七月自己今天住在哪里做了什么,告诉她自己过得多么多么的潇洒,当然最有都无一不漏的附上:“问候家明”,这成为七月的心病。
终于,安生终于受不了颠沛流离的生活,她跟七月说,她想回家。正在此时,家明想抛下七月去北京,他说不想辜负青春,要去拼搏一番。七月没有拦着他,就像当初没有拦着安生一样。就这样,家明去找安生了,安生回来了。久别重逢的闺蜜两人却发现即使没有了家明,他们也不能像以前一样没有嫌隙的生活了。一个受到良好教育衣食无忧的小公主和一个四海为家无依无靠的灰姑凉,她们的生活注定无法无缝融合,七月受不了安生那种凡事耍小聪明的性子,却不知道倘若不如此,安生早已在他乡沦落。她们大吵了一架,安生再次离开,这一次七月也没有阻拦。
回到北京的安生在后来遇到了家明,毫无疑问,他们同居了。可女人的第六感使得七月即使在千里之外也能感受到家明的背叛。当七月出现在家明门前的时候,家明正搂着安生。
此刻,仍然是无所畏惧的安生,她仿佛一切都没有发生一样,跟着七月进屋,家明被挡在门外,女人的事男人也不能插手。受伤的七月质问自己多年的闺蜜,尽管她们已经早已不像当年那样亲密无间。影片的高潮部分就在这里,安生与七月相互责备对方,用刺骨的言语告诉对方这些年来对方是如何用“装”来获取同情,从见面的那一瞬间开始,像两个爱哭的孩子一样,她们撕下了对彼此最后一层的伪装。
七月警告家明回家与她结婚,否则将永不原谅她。家明终究抛下了七月,可事实上婚礼当天逃走了。因为七月对他说:“你还是离开吧,我不想嫁给不爱我的人”。从全局来看,此时七月应该已经怀孕了,家明离开以后,七月终于可以逃脱家人安排的一切,过上了七月曾经想过的生活,四处流浪,体验一生。
自此,七月与安生交换了生命,也是从这里开始七月与安生让人分不清到底谁是谁。在金马奖颁奖典礼上,两个被称为“最毫不做作女星”的女孩说: “七月与安生,本来就是一个人” 。故事的结局我已经完全记不清,因为即使是在我刚观影结束的那一刻,都险些分不清活着的是安生还是七月。当然最后,两个女孩从伪装和幻想中解脱出来,七月背弃了早已确定的人生路线,开始学会流浪,肚里带着家明的孩子四处流浪,步履蹒跚走过七月曾经走过的路,自然安生的“死期”也赋予了七月。此时的安生接住了七月的人生接力棒,恋爱、读书、工作,变成了最初的七月。
终于,七月难产了,27岁,正是安生自己的“催命符”来临之时,安生带着七月的孩子躲在城市的角落,以七月为笔名写下了往事。直到家明的出现,七月的孩子找到了家明,告诉了安生是“七月”的真相。一切尘埃落地,即使是到最后,安生仍然和七月一样死守着对彼此的承诺。
七月与安生的性格逆转堪称本片最难以琢磨之处,两人从最初的心照不宣到撕开伪装,每一幕都是那么的真实,影片中马思纯及周冬雨的表演更是细致入微,使我从中看到了很多不容易看到的真相,感谢作者及导演!接下来打算看纸质书籍。
离开家乡来到这陌生的城市已经两年多了,一年一次回家更是让我自心底将长沙当成了自己的第二个家。我在这里结实了很多有趣的人,偶尔回想起来竟还能挑动我的心神。大学时光,够我品味一生了。
2016年10月24日 学习编程一年的我
第一年,我带着贫穷和自强进入团学会,活跃和热心肠使我轻松游离在形形色色的人群中,那个时候我认识了很多很多的人,拥有很多很多梦想,却对自己的大学很迷茫,因为我累,我没有自由,我失去了自己的道。自我进入计算机专业来时我就笃定了自己的道,我要在大学四年达到很高的计算机水平!无论硬件还是软件。而滞留在组织中束缚了我的思想。不过我不得不感谢,大一一年来,在我没有电脑的时刻我亲密作战的朋友们,虽然已形如路人。
去年的暑假,我拥有了两个最令人激情澎湃的梦——爱情和电脑。他们冲击了我的思想,我如饥似渴,吮吸着渴望许久的知识,从前端到后端,从底层到上层,从语言到系统,我无所不用其极地学习。我碰到过很多有趣的语言,虽然没有一个算是精通,但自此我筑下了成为full-stack 开发者的梦。前期的浮躁贪多虽然使我失去了超越众人的机会,却令我更加自豪和淡定。这一年以来,我进步虽算不上神速,但已知足。一年的编程学习自然比不上那些入学前就已接触开发的同学,可我显然会超越的。
了解到git之后,我的编程思想有了很大程度的进化。开源和团队,是我此时认为的未来最重要的两样东西。接下来,我该做项目了,百利而无一害。
2016年12月09日 深层次的语言特性
上面的日记以git和做项目为结尾,事实上我也的确独立完成了一个项目,并全程使用git进行代码操作。由于该软件的版权以及隐私性质,我将在该项目的基础上把非核心部分(包括完成的可拓展工具及接口)抽离出来作为一个简单的项目开源。
这是我完成的第一个CS模式软件,全程使用JAVA开发,遇到了不少问题,不过常常是熬夜解决了,最多的问题便是国内资源的贫瘠,出于所使用的Tookit以及涉及JVM的知识只有国外才有解决方案,所以不得不依靠google以及我这差的不能再差的英文。另外的问题就是语言特性的问题,第一次开发桌面应用程序,设计到软件工程的许多知识(虽然仍然只是九牛一毛),我不得不从头到尾思考整个软件的基础架构及层间联系,往往需要细微到每一个类与接口。自己虽然最终给出了解决方案,但由于语言特性及算法限制的原因终归存在许多潜在bug和内存分配问题(这在开源版本中会尽可能解决),所以开始阅读《effective Java》,刚看完十几页,书籍讲解的很细致,也拓展了很多Java语言底层的结构,此时我不得不考虑语言特性所带来的知识附加,希望我能通过阅读次数得出一个清晰优美且逻辑严密的软件。
一开始选择使用Java语言是由于它的跨平台性,做web开发的程序员总是希望桌面应用软件也能容纳平台差异。事实上也是如此,java具备不错的跨平台能力,但我觉得还不够,即使我现在的软件能实现多平台运行,可我需要付出的代价也是不小的,适应性Jar包和Jre使得真正跨平台的只是Java语言,这兴许最终会有个更好的方案,我相信Java,也相信自己。
下一步自然是准备项目的优化重构及开源。。。。
]]>这种方法整理来自hexo官网
标签插件和 Front-matter 中的标签不同,它们是用于在文章中快速插入特定内容的插件。
###引用块
在文章中插入引言,可包含作者、来源和标题。
别号: quote
content
#####样例
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque hendrerit lacus ut purus iaculis feugiat. Sed nec tempor elit, quis aliquam neque. Curabitur sed diam eget dolor fermentum semper at eu lorem.
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque hendrerit lacus ut purus iaculis feugiat. Sed nec tempor elit, quis aliquam neque. Curabitur sed diam eget dolor fermentum semper at eu lorem.
Do not just seek happiness for yourself. Seek happiness for all. Through kindness. Through mercy.
Do not just seek happiness for yourself. Seek happiness for all. Through kindness. Through mercy.
David LevithanWide Awake
NEW: DevDocs now comes with syntax highlighting. http://devdocs.io
NEW: DevDocs now comes with syntax highlighting. http://devdocs.io
@DevDocstwitter.com/devdocs/status/356095192085962752
Every interaction is both precious and an opportunity to delight.
Every interaction is both precious and an opportunity to delight.
Seth GodinWelcome to Island Marketing
#####代码块
在文章中插入代码。
别名: code
1
code snippet
样例
普通的代码块
1
alert('Hello World!');
alert(‘Hello World!’);
指定语言
1
[rectangle setX: 10 y: 10 width: 20 height: 20];
[rectangle setX: 10 y: 10 width: 20 height: 20];
附加说明1
array.map(callback[, thisArg])
Array.map
array.map(callback[, thisArg])
附加说明和网址1
2_.compact([0, 1, false, 2, '', 3]);
=> [1, 2, 3]
_.compactUnderscore.js
_.compact([0, 1, false, 2, ‘’, 3]);
=> [1, 2, 3]
反引号代码块
另一种形式的代码块,不同的是它使用三个反引号来包裹。[language] [title] [url] [link text] code snippet
Pull Quote
在文章中插入 Pull quote。
content
Link
在文章中插入链接,并自动给外部链接添加 target=”_blank”
属性。
text url [external] [title]