分类 技术 下的文章

下面是一些针对刚开始使用 Linux 命令行的人的热身练习。警告:它可能会上瘾。

如果你是 Linux 新手,或者从来没有花时间研究过命令行,你可能不会理解为什么这么多 Linux 爱好者坐在舒适的桌面前兴奋地输入命令来使用大量工具和应用。在这篇文章中,我们将快速浏览一下命令行的奇妙之处,看看能否让你着迷。

首先,要使用命令行,你必须打开一个命令工具(也称为“命令提示符”)。如何做到这一点将取决于你运行的 Linux 版本。例如,在 RedHat 上,你可能会在屏幕顶部看到一个 “Activities” 选项卡,它将打开一个选项列表和一个用于输入命令的小窗口(类似 “cmd” 为你打开的窗口)。在 Ubuntu 和其他一些版本中,你可能会在屏幕左侧看到一个小的终端图标。在许多系统上,你可以同时按 Ctrl+Alt+t 键打开命令窗口。

如果你使用 PuTTY 之类的工具登录 Linux 系统,你会发现自己已经处于命令行界面。

一旦你得到你的命令行窗口,你会发现自己坐在一个提示符面前。它可能只是一个 $ 或者像 user@system:~$ 这样的东西,但它意味着系统已经准备好为你运行命令了。

一旦你走到这一步,就应该开始输入命令了。下面是一些要首先尝试的命令,以及这里是一些特别有用的命令的 PDF 和适合打印和做成卡片的双面命令手册。

命令用途
pwd显示我在文件系统中的位置(在最初进入系统时运行将显示主目录)
ls列出我的文件
ls -a列出我更多的文件(包括隐藏文件)
ls -al列出我的文件,并且包含很多详细信息(包括日期、文件大小和权限)
who告诉我谁登录了(如果只有你,不要失望)
date日期提醒我今天是星期几(也显示时间)
ps列出我正在运行的进程(可能只是你的 shell 和 ps 命令)

一旦你从命令行角度习惯了 Linux 主目录之后,就可以开始探索了。也许你会准备好使用以下命令在文件系统中闲逛:

命令用途
cd /tmp移动到其他文件夹(本例中,打开 /tmp 文件夹)
ls列出当前位置的文件
cd回到主目录(不带参数的 cd 总是能将你带回到主目录)
cat .bashrc显示文件的内容(本例中显示 .bashrc 文件的内容)
history显示最近执行的命令
echo hello跟自己说 “hello”
cal显示当前月份的日历

要了解为什么高级 Linux 用户如此喜欢命令行,你将需要尝试其他一些功能,例如重定向和管道。“重定向”是当你获取命令的输出并将其放到文件中而不是在屏幕上显示时。“管道”是指你将一个命令的输出发送给另一条将以某种方式对其进行操作的命令。这是可以尝试的命令:

命令用途
echo "echo hello" > tryme创建一个新的文件并将 “echo hello” 写入该文件
chmod 700 tryme使新建的文件可执行
tryme运行新文件(它应当运行文件中包含的命令并且显示 “hello” )
ps aux显示所有运行中的程序
`ps auxgrep $USER`显示所有运行中的程序,但是限制输出的内容包含你的用户名
echo $USER使用环境变量显示你的用户名
whoami使用命令显示你的用户名
`whowc -l`计数所有当前登录的用户数目

总结

一旦你习惯了基本命令,就可以探索其他命令并尝试编写脚本。 你可能会发现 Linux 比你想象的要强大并且好用得多.


via: https://www.networkworld.com/article/3518440/intro-to-the-linux-command-line.html

作者:Sandra Henry-Stocker 选题:lujun9972 译者:qianmingtian 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

GNU/Linux 的好玩,真是有目共睹,世界上越来越多的用户已经使用和玩耍它很久啦。

今天 Linux 已经成为了一个庞大的生态链系统。从树莓派到超级计算机,从网络交换机到 SDN 都有 Linux 操作系统的身影。其实 GNU/Linux 系统本身指的是很多 GNU 自由开源软件和 Linux 内核的集合体。各种 Linux 发行版可以让你满足各种独特或者大众的功能。Linux 系统是当前系统管理员或者网络管理员尽量要熟悉的操作系统之一。当然,Linux 也提供了很多好玩的工具软件,我们以 Debian GNU/Linux 8.7 为例进行配置,将你的电脑终端命令行改变成一个就像电影里黑客的界面,装逼很有用哦。

配置 Vim,添加语法高亮及其它插件

因为 Debian GNU/Linux 系统已经默认使用了 Vim 7.4 版本,可以使用命令 vi /etc/vim/vimrc,添加如下内容:

syntax on
fileype plugin indent on
set hlsearch

这三个选项直接打开语法和搜索高亮显示,文件类型检测等功能。

安装 Vim 相关插件,可以使用命令:

apt-get install vim-addon-manager vim-addon-mw-utils \
  vim-gocomplete vim-erlang vim-erlang-syntax vim-gnome \
  vim-gocomplete vim-perl vim-python vim-ruby vim-scripts \
  vim-syntax-docker vim-syntax-go vim-syntax-gtk vim-tcl \
  vim-vimerl vim-vimerl-syntax vim-vimoutliner vim-youcompleteme

配置 root 用户本地 Bash 环境变量支持颜色显示

使用命令 vi ~/.bashrc,将如下内容:

alias ls='ls $LS_OPTIONS'
alias ll='ls $LS_OPTIONS -l'
alias l='ls $LS_OPTIONS -lA'

三行前的 # 号去掉。

安装 guake、screen 并进行配置

安装配置 guake 终端,使用命令 apt-get install guake 完成安装。

将其加入 gnome3 的开机启动程序,之后只要使用功能键 F12 就可以直接唤入唤出该终端进行命令行操作。F12 唤出后可右键点击首选项进行设置,建议用户参考如图配置:

安装 screen,使用命令 apt-get install screen 即可完成。

此时用户 guake 终端默认解释器列表中将出现 /usr/bin/screen 选项,用户可以直接指定。再次使用 F12 唤出 guake 终端就可以直接使用 screen 快捷键进行分屏操作了。

screen 常用快捷键如下:

  • Ctrl+a S 水平分割当前窗口
  • Ctrl+a | 垂直分割当前窗口
  • Ctrl+a c 创建一个新的运行 shell 的窗口并切换到该窗口
  • Ctrl+a X 关闭当前窗口
  • Ctrl+a x 锁定当前窗口
  • Ctrl+a k 杀死当前窗口

完成效果图如下:

修改提示符

使用命令 vi /etc/bash.bashrc, 将 PS1 内容修改如下:

PS1='\[\e[34;1m\]\t${debian_chroot:+($debian_chroot)}\[\e[32;1m\][\[\e[33;1m\]\u\[\e[31;1m\]@\[\e[33;1m\]\h \[\e[36;1m\]\w\[\e[32;1m\]]\[\e[34;1m\]\$ \[\e[0m\]'

使用命令 vi ~/.bashrc,查找 if [ "$color_prompt" = yes ]; then,并按照如下示例修改当前用户 PS1 内容。

if [ "$color_prompt" = yes ]; then
    PS1='${debian_chroot:+($debian_chroot)}\[\033[01;32m\]\u@\h\[\033[00m\]:\[\033[01;34m\]\w\[\033[00m\]\$ '
else
   #PS1='${debian_chroot:+($debian_chroot)}\u@\h:\w\$ '
   PS1='\[\e[34;1m\]\t${debian_chroot:+($debian_chroot)}\[\e[32;1m\][\[\e[33;1m\]\u\[\e[31;1m\]@\[\e[33;1m\]\h \[\e[36;1m\]\w\[\e[32;1m\]]\[\e[34;1m\]\$ \[\e[0m\]'
fi

试试看吧,你的 GNU/Linux 命令行会很惊艳哦!!!

FreeBSD 是一个开源操作系统,衍生自著名的 伯克利软件套件 Berkeley Software Distribution (BSD)。FreeBSD 的第一个版本发布于 1993 年,并且仍然在继续发展。2007 年左右,Lucas Holt 想要利用 OpenStep(现在是 Cocoa)的 Objective-C 框架、widget 工具包和应用程序开发工具的 GnuStep 实现,来创建一个 FreeBSD 的分支。为此,他开始开发 MidnightBSD 桌面发行版。

MidnightBSD(以 Lucas 的猫 Midnight 命名)仍然在积极地(尽管缓慢)开发。从 2017 年 8 月开始,可以获得最新的稳定发布版本(0.8.6)(LCTT 译注:截止至本译文发布时,当前是 2019/10/31 发布的 1.2 版)。尽管 BSD 发行版不是你所说的用户友好型发行版,但上手安装是熟悉如何处理 文本(ncurses)安装过程以及通过命令行完成安装的好方法。

这样,你最终会得到一个非常可靠的 FreeBSD 分支的桌面发行版。这需要花费一点精力,但是如果你是一名正在寻找扩展你的技能的 Linux 用户……这是一个很好的起点。

我将带你走过安装 MidnightBSD 的流程,如何添加一个图形桌面环境,然后如何安装应用程序。

安装

正如我所提到的,这是一个文本(ncurses)安装过程,因此在这里找不到可以用鼠标点击的地方。相反,你将使用你键盘的 Tab 键和箭头键。在你下载最新的发布版本后,将它刻录到一个 CD/DVD 或 USB 驱动器,并启动你的机器(或者在 VirtualBox 中创建一个虚拟机)。安装程序将打开并给你三个选项(图 1)。使用你的键盘的箭头键选择 “Install”,并敲击回车键。

 title=

图 1: 启动 MidnightBSD 安装程序。

在这里要经历相当多的屏幕。其中很多屏幕是一目了然的:

  1. 设置非默认键盘映射(是/否)
  2. 设置主机名称
  3. 添加可选系统组件(文档、游戏、32 位兼容性、系统源码代码)
  4. 对硬盘分区
  5. 管理员密码
  6. 配置网络接口
  7. 选择地区(时区)
  8. 启用服务(例如 ssh)
  9. 添加用户(图 2)

 title=

图 2: 向系统添加一个用户。

在你向系统添加用户后,你将被进入到一个窗口中(图 3),在这里,你可以处理任何你可能忘记配置或你想重新配置的东西。如果你不需要作出任何更改,选择 “Exit”,然后你的配置就会被应用。

 title=

图 3: 应用你的配置。

在接下来的窗口中,当出现提示时,选择 “No”,接下来系统将重启。在 MidnightBSD 重启后,你已经为下一阶段的安装做好了准备。

后安装阶段

当你最新安装的 MidnightBSD 启动时,你将发现你自己处于命令提示符当中。此刻,还没有图形界面。要安装应用程序,MidnightBSD 依赖于 mport 工具。比如说你想安装 Xfce 桌面环境。为此,登录到 MidnightBSD 中,并发出下面的命令:

sudo mport index
sudo mport install xorg

你现在已经安装好 Xorg 窗口服务器了,它允许你安装桌面环境。使用命令来安装 Xfce :

sudo mport install xfce

现在 Xfce 已经安装好。不过,我们必须让它同命令 startx 一起启用。为此,让我们先安装 nano 编辑器。发出命令:

sudo mport install nano

随着 nano 安装好,发出命令:

nano ~/.xinitrc

这个文件仅包含一行内容:

exec startxfce4

保存并关闭这个文件。如果你现在发出命令 startx, Xfce 桌面环境将会启动。你应该会感到有点熟悉了吧(图 4)。

 title=

图 4: Xfce 桌面界面已准备好服务。

因为你不会总是想必须发出命令 startx,你希望启用登录守护进程。然而,它却没有安装。要安装这个子系统,发出命令:

sudo mport install mlogind

当完成安装后,通过在 /etc/rc.conf 文件中添加一个项目来在启动时启用 mlogind。在 rc.conf 文件的底部,添加以下内容:

mlogind_enable=”YES”

保存并关闭该文件。现在,当你启动(或重启)机器时,你应该会看到图形登录屏幕。在写这篇文章的时候,在登录后我最后得到一个空白屏幕和讨厌的 X 光标。不幸的是,目前似乎并没有这个问题的解决方法。所以,要访问你的桌面环境,你必须使用 startx 命令。

安装应用

默认情况下,你找不到很多能可用的应用程序。如果你尝试使用 mport 安装应用程序,你很快就会感到沮丧,因为只能找到很少的应用程序。为解决这个问题,我们需要使用 svnlite 命令来查看检出的可用 mport 软件列表。回到终端窗口,并发出命令:

svnlite co http://svn.midnightbsd.org/svn/mports/trunk mports

在你完成这些后,你应该看到一个命名为 ~/mports 的新目录。使用命令 cd ~/.mports 更改到这个目录。发出 ls 命令,然后你应该看到许多的类别(图 5)。

 title=

图 5: mport 现在可用的应用程序类别。

你想安装 Firefox 吗?如果你查看 www 目录,你将看到一个 linux-firefox 列表。发出命令:

sudo mport install linux-firefox

现在你应该会在 Xfce 桌面菜单中看到一个 Firefox 项。翻找所有的类别,并使用 mport 命令来安装你需要的所有软件。

一个悲哀的警告

一个悲哀的小警告是,mport (通过 svnlite)仅能找到的一个办公套件的版本是 OpenOffice 3 。那是非常过时的。尽管在 ~/mports/editors 目录中能找到 Abiword ,但是它看起来不能安装。甚至在安装 OpenOffice 3 后,它会输出一个执行格式错误。换句话说,你不能使用 MidnightBSD 在办公生产效率方面做很多的事情。但是,嘿嘿,如果你周围正好有一个旧的 Palm Pilot,你可以安装 pilot-link。换句话说,可用的软件不足以构成一个极其有用的桌面发行版……至少对普通用户不是。但是,如果你想在 MidnightBSD 上开发,你将找到很多可用的工具可以安装(查看 ~/mports/devel 目录)。你甚至可以使用命令安装 Drupal :

sudo mport install drupal7

当然,在此之后,你将需要创建一个数据库(MySQL 已经安装)、安装 Apache(sudo mport install apache24),并配置必要的 Apache 配置。

显然地,已安装的和可以安装的是一个应用程序、系统和服务的大杂烩。但是随着足够多的工作,你最终可以得到一个能够服务于特殊目的的发行版。

享受 *BSD 优良

这就是如何使 MidnightBSD 启动,并使其运行某种有用的桌面发行版的方法。它不像很多其它的 Linux 发行版一样快速简便,但是如果你想要一个促使你思考的发行版,这可能正是你正在寻找的。尽管大多数竞争对手都准备了很多可以安装的应用软件,但 MidnightBSD 无疑是一个 Linux 爱好者或管理员应该尝试的有趣挑战。


via: https://www.linux.com/learn/intro-to-linux/2018/5/midnightbsd-could-be-your-gateway-freebsd

作者:Jack Wallen 选题:lujun9972 译者:robsean 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

第一篇文章中,我提到,项目的自动部署是放在 now.sh 上,以方便预览。但出于用户体验和速度的考虑,我们选择了国内的七牛云作为页面的承载。不过,七牛毕竟是一个对象存储,而不是一个专业的静态托管业务,在使用上遇到了一些坑,还好经过努力都得到了解决。

七牛的 Bucket 名规则

和绝大多数的云计算厂商一样,七牛也使用了 Bucket 来作为存储的单元。

由于这个项目要挂 Linux.cn 的二级域名,于是我便让老王创建了一个 Bucket,绑定域名,并通过七牛自带的权限控制机制,将其分发给我,让我来使用。

在我的个人控制台看到了这个 Bucket:

发现问题

我通过控制台,手动上传了生成的文件后,确认没有问题,就将相应的功能写入到 Github Action 的 配置文件(配置文件点这里)中,实现自动的部署。但在部署过程中,屡次报错,不知道为什么。在开启了 DEBUG 信息后发现,竟然是 Bucket 不存在(点我查看 CI 的构建信息)。

解决问题

和老王沟通以后才发现,是七牛的 Bucket 名机制的问题。

在七牛中进行权限分配的时候,会要求你为 Bucket 设定一个别名,而且名字和已有的名字必须是不同的,这导致我看到的 Bucket 的名和老王创建的 Bucket 名是不相同的。

而我使用的 AK 和 SK 又是让老王设置在 Github 后台的 Secrets,Bucket 则是我自己设置的,所以就出现了问题。具体来说,是下面这张图。

由于我填写的 Bucket 是我自己看到的,而不是老王那边真正的 Bucket 名称,导致在上传的时候,无法找到 Bucket。在将 Bucket 名称替换为老王那边看到的 Bucket 名称后,问题得到解决。

七牛不支持 Vue Router 的 History 模式

第二篇文章中,我提到了引入了 Vue 的 History 模式来优化体验。但是,七牛本身作为一个存储系统,没有转发的功能,也就导致其没有办法很好的支持 Vue History 模式。

在经过一番研究后,找到了解决方案,就是将 index 页面,同时作为 404 页面,这样就可以实现从某种意义上的将所有请求都转发给 Index 页面。

你需要做的,就是将 index.html 复制一份,并重命名为 errno-404,并和其他文件一同上传,这样用户请求一些不存在的文件时,会自动将请求转发给 errno-404, 又因为这个文件的内容是索引文件的内容,所以就可以实现了请求的转发。

相关代码的实现,你可以在 https://github.com/LCTT/tldr.linux.cn/blob/master/.github/workflows/nodejs.yml 这里找到。

总结

在这篇文章中,介绍了七牛的 Bucket 问题,以及 Vue Router History 模式在七牛下的解决方案。

学习如何使用 Java 8 中的流 API 和函数式编程结构。

当 Java SE 8(又名核心 Java 8)在 2014 年被推出时,它引入了一些更改,从根本上影响了用它进行的编程。这些更改中有两个紧密相连的部分:流 API 和函数式编程构造。本文使用代码示例,从基础到高级特性,介绍每个部分并说明它们之间的相互作用。

基础特性

流 API 是在数据序列中迭代元素的简洁而高级的方法。包 java.util.streamjava.util.function 包含了用于流 API 和相关函数式编程构造的新库。当然,代码示例胜过千言万语。

下面的代码段用大约 2,000 个随机整数值填充了一个 List

Random rand = new Random2();
List<Integer> list = new ArrayList<Integer>();           // 空 list
for (int i = 0; i < 2048; i++) list.add(rand.nextInt()); // 填充它

另外用一个 for 循环可用于遍历填充列表,以将偶数值收集到另一个列表中。

流 API 提供了一种更简洁的方法来执行此操作:

List <Integer> evens = list
    .stream()                      // 流化 list
    .filter(n -> (n & 0x1) == 0)   // 过滤出奇数值
    .collect(Collectors.toList()); // 收集偶数值

这个例子有三个来自流 API 的函数:

  • stream 函数可以将集合转换为流,而流是一个每次可访问一个值的传送带。流化是惰性的(因此也是高效的),因为值是根据需要产生的,而不是一次性产生的。
  • filter 函数确定哪些流的值(如果有的话)通过了处理管道中的下一个阶段,即 collect 阶段。filter 函数是 高阶的 higher-order ,因为它的参数是一个函数 —— 在这个例子中是一个 lambda 表达式,它是一个未命名的函数,并且是 Java 新的函数式编程结构的核心。

lambda 语法与传统的 Java 完全不同:

n -> (n & 0x1) == 0

箭头(一个减号后面紧跟着一个大于号)将左边的参数列表与右边的函数体分隔开。参数 n 虽未明确类型,但也可以明确。在任何情况下,编译器都会发现 n 是个 Integer。如果有多个参数,这些参数将被括在括号中,并用逗号分隔。

在本例中,函数体检查一个整数的最低位(最右)是否为零,这用来表示偶数。过滤器应返回一个布尔值。尽管可以,但该函数的主体中没有显式的 return。如果主体没有显式的 return,则主体的最后一个表达式即是返回值。在这个例子中,主体按照 lambda 编程的思想编写,由一个简单的布尔表达式 (n & 0x1) == 0 组成。

  • collect 函数将偶数值收集到引用为 evens 的列表中。如下例所示,collect 函数是线程安全的,因此,即使在多个线程之间共享了过滤操作,该函数也可以正常工作。

方便的功能和轻松实现多线程

在生产环境中,数据流的源可能是文件或网络连接。为了学习流 API, Java 提供了诸如 IntStream 这样的类型,它可以用各种类型的元素生成流。这里有一个 IntStream 的例子:

IntStream                          // 整型流
    .range(1, 2048)                // 生成此范围内的整型流
    .parallel()                    // 为多个线程分区数据
    .filter(i -> ((i & 0x1) > 0))  // 奇偶校验 - 只允许奇数通过
    .forEach(System.out::println); // 打印每个值

IntStream 类型包括一个 range 函数,该函数在指定的范围内生成一个整数值流,在本例中,以 1 为增量,从 1 递增到 2048。parallel 函数自动划分该工作到多个线程中,在各个线程中进行过滤和打印。(线程数通常与主机系统上的 CPU 数量匹配。)函数 forEach 参数是一个方法引用,在本例中是对封装在 System.out 中的 println 方法的引用,方法输出类型为 PrintStream。方法和构造器引用的语法将在稍后讨论。

由于具有多线程,因此整数值整体上以任意顺序打印,但在给定线程中是按顺序打印的。例如,如果线程 T1 打印 409 和 411,那么 T1 将按照顺序 409-411 打印,但是其它某个线程可能会预先打印 2045。parallel 调用后面的线程是并发执行的,因此它们的输出顺序是不确定的。

map/reduce 模式

map/reduce 模式在处理大型数据集方面变得很流行。一个 map/reduce 宏操作由两个微操作构成。首先,将数据分散( 映射 mapped )到各个工作程序中,然后将单独的结果收集在一起 —— 也可能收集统计起来成为一个值,即 归约 reduction 。归约可以采用不同的形式,如以下示例所示。

下面 Number 类的实例用 EVENODD 表示有奇偶校验的整数值:

public class Number {
    enum Parity { EVEN, ODD }
    private int value;
    public Number(int n) { setValue(n); }
    public void setValue(int value) { this.value = value; }
    public int getValue() { return this.value; }
    public Parity getParity() {
        return ((value & 0x1) == 0) ? Parity.EVEN : Parity.ODD;
    }
    public void dump() {
        System.out.format("Value: %2d (parity: %s)\n", getValue(),
                          (getParity() == Parity.ODD ? "odd" : "even"));
    }
}

下面的代码演示了用 Number 流进行 map/reduce 的情形,从而表明流 API 不仅可以处理 intfloat 等基本类型,还可以处理程序员自定义的类类型。

在下面的代码段中,使用了 parallelStream 而不是 stream 函数对随机整数值列表进行流化处理。与前面介绍的 parallel 函数一样,parallelStream 变体也可以自动执行多线程。

final int howMany = 200;
Random r = new Random();
Number[] nums = new Number[howMany];
for (int i = 0; i < howMany; i++) nums[i] = new Number(r.nextInt(100));
List<Number> listOfNums = Arrays.asList(nums);  // 将数组转化为 list

Integer sum4All = listOfNums
    .parallelStream()           // 自动执行多线程
    .mapToInt(Number::getValue) // 使用方法引用,而不是 lambda
    .sum();                     // 将流值计算出和值
System.out.println("The sum of the randomly generated values is: " + sum4All);

高阶的 mapToInt 函数可以接受一个 lambda 作为参数,但在本例中,它接受一个方法引用,即 Number::getValuegetValue 方法不需要参数,它返回给定的 Number 实例的 int 值。语法并不复杂:类名 Number 后跟一个双冒号和方法名。回想一下先前的例子 System.out::println,它在 System 类中的 static 属性 out 后面有一个双冒号。

方法引用 Number::getValue 可以用下面的 lambda 表达式替换。参数 n 是流中的 Number 实例中的之一:

mapToInt(n -> n.getValue())

通常,lambda 表达式和方法引用是可互换的:如果像 mapToInt 这样的高阶函数可以采用一种形式作为参数,那么这个函数也可以采用另一种形式。这两个函数式编程结构具有相同的目的 —— 对作为参数传入的数据执行一些自定义操作。在两者之间进行选择通常是为了方便。例如,lambda 可以在没有封装类的情况下编写,而方法则不能。我的习惯是使用 lambda,除非已经有了适当的封装方法。

当前示例末尾的 sum 函数通过结合来自 parallelStream 线程的部分和,以线程安全的方式进行归约。但是,程序员有责任确保在 parallelStream 调用引发的多线程过程中,程序员自己的函数调用(在本例中为 getValue)是线程安全的。

最后一点值得强调。lambda 语法鼓励编写 纯函数 pure function ,即函数的返回值仅取决于传入的参数(如果有);纯函数没有副作用,例如更新一个类中的 static 字段。因此,纯函数是线程安全的,并且如果传递给高阶函数的函数参数(例如 filtermap )是纯函数,则流 API 效果最佳。

对于更细粒度的控制,有另一个流 API 函数,名为 reduce,可用于对 Number 流中的值求和:

Integer sum4AllHarder = listOfNums
    .parallelStream()                           // 多线程
    .map(Number::getValue)                      // 每个 Number 的值
    .reduce(0, (sofar, next) -> sofar + next);  // 求和

此版本的 reduce 函数带有两个参数,第二个参数是一个函数:

  • 第一个参数(在这种情况下为零)是特征值,该值用作求和操作的初始值,并且在求和过程中流结束时用作默认值。
  • 第二个参数是累加器,在本例中,这个 lambda 表达式有两个参数:第一个参数(sofar)是正在运行的和,第二个参数(next)是来自流的下一个值。运行的和以及下一个值相加,然后更新累加器。请记住,由于开始时调用了 parallelStream,因此 mapreduce 函数现在都在多线程上下文中执行。

在到目前为止的示例中,流值被收集,然后被规约,但是,通常情况下,流 API 中的 Collectors 可以累积值,而不需要将它们规约到单个值。正如下一个代码段所示,收集活动可以生成任意丰富的数据结构。该示例使用与前面示例相同的 listOfNums

Map<Number.Parity, List<Number>> numMap = listOfNums
    .parallelStream()
    .collect(Collectors.groupingBy(Number::getParity));

List<Number> evens = numMap.get(Number.Parity.EVEN);
List<Number> odds = numMap.get(Number.Parity.ODD);

第一行中的 numMap 指的是一个 Map,它的键是一个 Number 奇偶校验位(ODDEVEN),其值是一个具有指定奇偶校验位值的 Number 实例的 List。同样,通过 parallelStream 调用进行多线程处理,然后 collect 调用(以线程安全的方式)将部分结果组装到 numMap 引用的 Map 中。然后,在 numMap 上调用 get 方法两次,一次获取 evens,第二次获取 odds

实用函数 dumpList 再次使用来自流 API 的高阶 forEach 函数:

private void dumpList(String msg, List<Number> list) {
    System.out.println("\n" + msg);
    list.stream().forEach(n -> n.dump()); // 或者使用 forEach(Number::dump)
}

这是示例运行中程序输出的一部分:

The sum of the randomly generated values is: 3322
The sum again, using a different method:     3322

Evens:

Value: 72 (parity: even)
Value: 54 (parity: even)
...
Value: 92 (parity: even)

Odds:

Value: 35 (parity: odd)
Value: 37 (parity: odd)
...
Value: 41 (parity: odd)

用于代码简化的函数式结构

函数式结构(如方法引用和 lambda 表达式)非常适合在流 API 中使用。这些构造代表了 Java 中对高阶函数的主要简化。即使在糟糕的过去,Java 也通过 MethodConstructor 类型在技术上支持高阶函数,这些类型的实例可以作为参数传递给其它函数。由于其复杂性,这些类型在生产级 Java 中很少使用。例如,调用 Method 需要对象引用(如果方法是非静态的)或至少一个类标识符(如果方法是静态的)。然后,被调用的 Method 的参数作为对象实例传递给它,如果没有发生多态(那会出现另一种复杂性!),则可能需要显式向下转换。相比之下,lambda 和方法引用很容易作为参数传递给其它函数。

但是,新的函数式结构在流 API 之外具有其它用途。考虑一个 Java GUI 程序,该程序带有一个供用户按下的按钮,例如,按下以获取当前时间。按钮按下的事件处理程序可能编写如下:

JButton updateCurrentTime = new JButton("Update current time");
updateCurrentTime.addActionListener(new ActionListener() {
    @Override
    public void actionPerformed(ActionEvent e) {
        currentTime.setText(new Date().toString());
    }
});

这个简短的代码段很难解释。关注第二行,其中方法 addActionListener 的参数开始如下:

new ActionListener() {

这似乎是错误的,因为 ActionListener 是一个抽象接口,而抽象类型不能通过调用 new 实例化。但是,事实证明,还有其它一些实例被实例化了:一个实现此接口的未命名内部类。如果上面的代码封装在名为 OldJava 的类中,则该未命名的内部类将被编译为 OldJava$1.classactionPerformed 方法在这个未命名的内部类中被重写。

现在考虑使用新的函数式结构进行这个令人耳目一新的更改:

updateCurrentTime.addActionListener(e -> currentTime.setText(new Date().toString()));

lambda 表达式中的参数 e 是一个 ActionEvent 实例,而 lambda 的主体是对按钮上的 setText 的简单调用。

函数式接口和函数组合

到目前为止,使用的 lambda 已经写好了。但是,为了方便起见,我们可以像引用封装方法一样引用 lambda 表达式。以下一系列简短示例说明了这一点。

考虑以下接口定义:

@FunctionalInterface // 可选,通常省略
interface BinaryIntOp {
    abstract int compute(int arg1, int arg2); // abstract 声明可以被删除
}

注释 @FunctionalInterface 适用于声明唯一抽象方法的任何接口;在本例中,这个抽象接口是 compute。一些标准接口,(例如具有唯一声明方法 runRunnable 接口)同样符合这个要求。在此示例中,compute 是已声明的方法。该接口可用作引用声明中的目标类型:

BinaryIntOp div = (arg1, arg2) -> arg1 / arg2;
div.compute(12, 3); // 4

java.util.function 提供各种函数式接口。以下是一些示例。

下面的代码段介绍了参数化的 Predicate 函数式接口。在此示例中,带有参数 StringPredicate<String> 类型可以引用具有 String 参数的 lambda 表达式或诸如 isEmpty 之类的 String 方法。通常情况下,Predicate 是一个返回布尔值的函数。

Predicate<String> pred = String::isEmpty; // String 方法的 predicate 声明
String[] strings = {"one", "two", "", "three", "four"};
Arrays.asList(strings)
   .stream()
   .filter(pred)                  // 过滤掉非空字符串
   .forEach(System.out::println); // 只打印空字符串

在字符串长度为零的情况下,isEmpty Predicate 判定结果为 true。 因此,只有空字符串才能进入管道的 forEach 阶段。

下一段代码将演示如何将简单的 lambda 或方法引用组合成更丰富的 lambda 或方法引用。考虑这一系列对 IntUnaryOperator 类型的引用的赋值,它接受一个整型参数并返回一个整型值:

IntUnaryOperator doubled = n -> n * 2;
IntUnaryOperator tripled = n -> n * 3;
IntUnaryOperator squared = n -> n * n;

IntUnaryOperator 是一个 FunctionalInterface,其唯一声明的方法为 applyAsInt。现在可以单独使用或以各种组合形式使用这三个引用 doubledtripledsquared

int arg = 5;
doubled.applyAsInt(arg); // 10
tripled.applyAsInt(arg); // 15
squared.applyAsInt(arg); // 25

以下是一些函数组合的样例:

int arg = 5;
doubled.compose(squared).applyAsInt(arg); // 5 求 2 次方后乘 2:50
tripled.compose(doubled).applyAsInt(arg); // 5 乘 2 后再乘 3:30
doubled.andThen(squared).applyAsInt(arg); // 5 乘 2 后求 2 次方:100
squared.andThen(tripled).applyAsInt(arg); // 5 求 2 次方后乘 3:75

函数组合可以直接使用 lambda 表达式实现,但是引用使代码更简洁。

构造器引用

构造器引用是另一种函数式编程构造,而这些引用在比 lambda 和方法引用更微妙的上下文中非常有用。再一次重申,代码示例似乎是最好的解释方式。

考虑这个 POJO 类:

public class BedRocker { // 基岩的居民
    private String name;
    public BedRocker(String name) { this.name = name; }
    public String getName() { return this.name; }
    public void dump() { System.out.println(getName()); }
}

该类只有一个构造函数,它需要一个 String 参数。给定一个名字数组,目标是生成一个 BedRocker 元素数组,每个名字代表一个元素。下面是使用了函数式结构的代码段:

String[] names = {"Fred", "Wilma", "Peebles", "Dino", "Baby Puss"};

Stream<BedRocker> bedrockers = Arrays.asList(names).stream().map(BedRocker::new);
BedRocker[] arrayBR = bedrockers.toArray(BedRocker[]::new);

Arrays.asList(arrayBR).stream().forEach(BedRocker::dump);

在较高的层次上,这个代码段将名字转换为 BedRocker 数组元素。具体来说,代码如下所示。Stream 接口(在包 java.util.stream 中)可以被参数化,而在本例中,生成了一个名为 bedrockersBedRocker 流。

Arrays.asList 实用程序再次用于流化一个数组 names,然后将流的每一项传递给 map 函数,该函数的参数现在是构造器引用 BedRocker::new。这个构造器引用通过在每次调用时生成和初始化一个 BedRocker 实例来充当一个对象工厂。在第二行执行之后,名为 bedrockers 的流由五项 BedRocker 组成。

这个例子可以通过关注高阶 map 函数来进一步阐明。在通常情况下,一个映射将一个类型的值(例如,一个 int)转换为另一个相同类型的值(例如,一个整数的后继):

map(n -> n + 1) // 将 n 映射到其后继

然而,在 BedRocker 这个例子中,转换更加戏剧化,因为一个类型的值(代表一个名字的 String)被映射到一个不同类型的值,在这个例子中,就是一个 BedRocker 实例,这个字符串就是它的名字。转换是通过一个构造器调用来完成的,它是由构造器引用来实现的:

map(BedRocker::new) // 将 String 映射到 BedRocker

传递给构造器的值是 names 数组中的其中一项。

此代码示例的第二行还演示了一个你目前已经非常熟悉的转换:先将数组先转换成 List,然后再转换成 Stream

Stream<BedRocker> bedrockers = Arrays.asList(names).stream().map(BedRocker::new);

第三行则是另一种方式 —— 流 bedrockers 通过使用数组构造器引用 BedRocker[]::new 调用 toArray 方法:

BedRocker[ ] arrayBR = bedrockers.toArray(BedRocker[]::new);

该构造器引用不会创建单个 BedRocker 实例,而是创建这些实例的整个数组:该构造器引用现在为 BedRocker[]:new,而不是 BedRocker::new。为了进行确认,将 arrayBR 转换为 List,再次对其进行流式处理,以便可以使用 forEach 来打印 BedRocker 的名字。

Fred
Wilma
Peebles
Dino
Baby Puss

该示例对数据结构的微妙转换仅用几行代码即可完成,从而突出了可以将 lambda,方法引用或构造器引用作为参数的各种高阶函数的功能。

柯里化 Currying

柯里化函数是指减少函数执行任何工作所需的显式参数的数量(通常减少到一个)。(该术语是为了纪念逻辑学家 Haskell Curry。)一般来说,函数的参数越少,调用起来就越容易,也更健壮。(回想一下一些需要半打左右参数的噩梦般的函数!)因此,应将柯里化视为简化函数调用的一种尝试。java.util.function 包中的接口类型适合于柯里化,如以下示例所示。

引用的 IntBinaryOperator 接口类型是为函数接受两个整型参数,并返回一个整型值:

IntBinaryOperator mult2 = (n1, n2) -> n1 * n2;
mult2.applyAsInt(10, 20); // 200
mult2.applyAsInt(10, 30); // 300

引用 mult2 强调了需要两个显式参数,在本例中是 10 和 20。

前面介绍的 IntUnaryOperatorIntBinaryOperator 简单,因为前者只需要一个参数,而后者则需要两个参数。两者均返回整数值。因此,目标是将名为 mult2 的两个参数 IntBinraryOperator 柯里化成一个单一的 IntUnaryOperator 版本 curriedMult2

考虑 IntFunction<R> 类型。此类型的函数采用整型参数,并返回类型为 R 的结果,该结果可以是另一个函数 —— 更准确地说,是 IntBinaryOperator。让一个 lambda 返回另一个 lambda 很简单:

arg1 -> (arg2 -> arg1 * arg2) // 括号可以省略

完整的 lambda 以 arg1 开头,而该 lambda 的主体以及返回的值是另一个以 arg2 开头的 lambda。返回的 lambda 仅接受一个参数(arg2),但返回了两个数字的乘积(arg1arg2)。下面的概述,再加上代码,应该可以更好地进行说明。

以下是如何柯里化 mult2 的概述:

  • 类型为 IntFunction<IntUnaryOperator> 的 lambda 被写入并调用,其整型值为 10。返回的 IntUnaryOperator 缓存了值 10,因此变成了已柯里化版本的 mult2,在本例中为 curriedMult2
  • 然后使用单个显式参数(例如,20)调用 curriedMult2 函数,该参数与缓存的参数(在本例中为 10)相乘以生成返回的乘积。。

这是代码的详细信息:

// 创建一个接受一个参数 n1 并返回一个单参数 n2 -> n1 * n2 的函数,该函数返回一个(n1 * n2 乘积的)整型数。
IntFunction<IntUnaryOperator> curriedMult2Maker = n1 -> (n2 -> n1 * n2);

调用 curriedMult2Maker 生成所需的 IntUnaryOperator 函数:

// 使用 curriedMult2Maker 获取已柯里化版本的 mult2。
// 参数 10 是上面的 lambda 的 n1。
IntUnaryOperator curriedMult2 = curriedMult2Maker2.apply(10);

10 现在缓存在 curriedMult2 函数中,以便 curriedMult2 调用中的显式整型参数乘以 10:

curriedMult2.applyAsInt(20); // 200 = 10 * 20
curriedMult2.applyAsInt(80); // 800 = 10 * 80

缓存的值可以随意更改:

curriedMult2 = curriedMult2Maker.apply(50); // 缓存 50
curriedMult2.applyAsInt(101);               // 5050 = 101 * 50

当然,可以通过这种方式创建多个已柯里化版本的 mult2,每个版本都有一个 IntUnaryOperator

柯里化充分利用了 lambda 的强大功能:可以很容易地编写 lambda 表达式来返回需要的任何类型的值,包括另一个 lambda。

总结

Java 仍然是基于类的面向对象的编程语言。但是,借助流 API 及其支持的函数式构造,Java 向函数式语言(例如 Lisp)迈出了决定性的(同时也是受欢迎的)一步。结果是 Java 更适合处理现代编程中常见的海量数据流。在函数式方向上的这一步还使以在前面的代码示例中突出显示的管道的方式编写清晰简洁的 Java 代码更加容易:

dataStream
   .parallelStream() // 多线程以提高效率
   .filter(...)      // 阶段 1
   .map(...)         // 阶段 2
   .filter(...)      // 阶段 3
   ...
   .collect(...);    // 或者,也可以进行归约:阶段 N

自动多线程,以 parallelparallelStream 调用为例,建立在 Java 的 fork/join 框架上,该框架支持 任务窃取 task stealing 以提高效率。假设 parallelStream 调用后面的线程池由八个线程组成,并且 dataStream 被八种方式分区。某个线程(例如,T1)可能比另一个线程(例如,T7)工作更快,这意味着应该将 T7 的某些任务移到 T1 的工作队列中。这会在运行时自动发生。

在这个简单的多线程世界中,程序员的主要职责是编写线程安全函数,这些函数作为参数传递给在流 API 中占主导地位的高阶函数。尤其是 lambda 鼓励编写纯函数(因此是线程安全的)函数。


via: https://opensource.com/article/20/1/javastream

作者:Marty Kalin 选题:lujun9972 译者:laingke 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

在完成了产品的基础开发以后,接下来需要进行一些周边的工作,这些周边工具将会帮助下一步优化产品。

为什么要加应用统计和 Crash 收集

不少开发者在开发的时候,很少会意识到需要添加应用统计和 Crash 收集。但对于一个合格的应用来说,这些是必须的。

  • 应用统计:应用统计会在后续进行产品迭代的时候给予数据的支持,让能够明确为什么要更新、要更新什么以及为什么这么做。
  • Crash 收集:人无完人,很难开发出一个完美的应用,就随时有可能会出现应用报错的情况。出现报错以后由于用户的水平不同,有效的反馈其实很少。Crash 收集则可以帮助收集必要的 Crash 信息,从而在后续开发的过程中,有针对性的修复 Bug。

应该使用哪些工具?

在应用统计领域,可选项非常多,大部分人使用的是 Google Analytics ,不过由于这个产品的面向用户主要是国内的用户,因此我更倾向选择加载速度更快的产品,最终我选择的是来自腾讯的移动应用分析(MTA),腾讯的移动应用分析中,提供了 HTML5 产品的接入,因此可以完成 TLDR 的统计。

在 Crash 收集方面,大家用的比较普遍的是 Sentry.io ,不过因为 Linux 中国并没有足够多的产品业务需要使用 Sentry 来收集 Crash ,因此,一直使用的是官网的免费使用版本。同样因为网络加载速度的原因,选择使用了国内的竞品 —— FunDebug

接入工具

腾讯移动应用分析

腾讯移动应用分析的接入并不复杂,首先,你需要登陆其官网,创建一个 HTML5 应用。

并在创建完成后,根据你自己的需要配置相应的能力,这里我开启了所有的数据统计,用以支持后续的产品迭代决策。

配置完成后,你会获得具体的统计的代码,接下来就可以进行接入。

一个比较简单的方法是直接将代码复制,并粘贴到 public/index.html 中,从而实现统计。不过,这样嵌入会导致如果需要自定义统计时,会无法通过 ESLint 的规则,因此我选择了第二种方式,使用 Vue 插件的方式接入。

使用 Vue 插件接入时,需要使用 mars-mta 这个包。

先使用 npm 安装依赖,然后在 main.js 中加入相应的统计代码,就可以实现自动的统计。

import Vue from 'vue'
import App from './App.vue'
import router from './router'
import vuetify from './plugins/vuetify';

// 以下为新增代码
import Mars from 'mars-mta'
Vue.use(Mars, {
    open: true, // 开关,若为false,则不会发出上报
    config: {
      sid: '500710182', // 必填,统计用的appid
      cid: '500710183', // 如果开启自定义事件,此项目为必填,否则不填
      autoReport: 1, // 是否开启自动上报(1:init完成则上报一次,0:使用pgv方法才上报)
      senseHash: 1, // hash锚点是否进入url统计
      senseQuery: 1, // url参数是否进入url统计
      performanceMonitor: 1, // 是否开启性能监控
    }
})
// 以上为新增代码

new Vue({
  router,
  vuetify,
  render: h => h(App),
  beforeCreate: async function(){
        const auth = this.$tcb.auth({ persistence: 'local' });
        await auth.signInAnonymously();
  }
}).$mount('#app')

在我的代码中,配置了 sidcid ,这些信息你都需要在腾讯 MTA 的应用管理后台获取。

而下方的配置,则根据你自己的实际需求选择开启即可。

对应的提交:https://github.com/LCTT/tldr.linux.cn/commit/61821aff4bf75fda3e81d96c6cd34a51efd00773

FunDebug

Fundebug 是我之前在开发小程序的时候用过的 Crash 收集应用。这次刚好也用上了。

因为预算的问题,这里我使用的是免费版本,有几个 tab 是看不到的,但是基本的 Debug 也是够用了。

Fundebug 的安装也很简单, 访问 https://www.fundebug.com/ ,注册账号, 并创建一个项目,你会获得一个 API Key,后续你可以使用这个 API Key 来初始化你的项目。

执行如下命令来安装依赖:

npm install fundebug-javascript fundebug-vue --save

并在 main.js 中添加如下代码(将 API-KEY 替换为你自己的 API KEY)并保存,就可以引入 Fundebug 来进行 Crash 收集了。

import * as fundebug from "fundebug-javascript";
import fundebugVue from "fundebug-vue";
fundebug.init({
    apikey: "API-KEY"
})
fundebugVue(fundebug, Vue);

对应的提交:https://github.com/LCTT/tldr.linux.cn/commit/225ca9d38e80eb55defac6383f5b9c228bdab6fe

优化

在开发的过程中经常会出现 Error,这个是难以避免的。在开启了 Crash 收集以后,这些 ERROR 也会被收集到 Fundebug 上,这样会浪费每个月 3000 条的免费额度,因此,需要一个方法在开发环境不启用这些拓展。类似的,也不希望 MTA 统计本地开发的 Page View ,会影响到后续的数据分析。因此,我使用了一些方法来避开这个问题。

if (process.env.NODE_ENV === 'production') {
    Vue.use(Mars, {
      open: true, // 开关,若为false,则不会发出上报
      config: {
        sid: 'xxx', // 必填,统计用的appid
      }
  })
  fundebug.apikey = "xxx"
  fundebugVue(fundebug, Vue);
}

通过将引用统计的代码包裹在环境的判断代码中,可以实现在渲染的时候,只有生产环境才会渲染出相应的统计代码,从而实现了在开发环境不调用 fundebug 和 mta 统计,避免了开发环境的数据干扰。

对应的提交:https://github.com/LCTT/tldr.linux.cn/commit/62f87b51fabd7c25cd905560157a546fd62babf2

总结

在这篇文章中,介绍了两个服务,分别是用于统计的腾讯移动分析 MTA 和用于做 Crash 收集的 fundebug,介绍了应该如何在 Vue 应用中接入这两种服务。此外,还根据实际的需求,优化了两个统计的位置,确保产品在开发环境不会工作,从而避免了影响到我们统计数据的准确性。

这篇文章涉及到的代码你都可以在 https://github.com/LCTT/tldr.linux.cn/blob/master/src/main.js 看到。