Yumiku

Yumiku 查看完整档案

杭州编辑浙江大学  |  计算机科学与技术 编辑IDEA Lab  |  程序员 编辑 rheabubbles.github.io/ 编辑
编辑

只想做一个简简单单的程序员,偶尔学点算法,尝试尽力但不一定能定期更新,主要是一些后端深入、算法基础讲解(经典算法和人工智能算法)。

目标是在2021年2月找到提前批实习
-`д´-

个人动态

Yumiku 发布了文章 · 2020-04-13

2021算法岗基础技能树怎么点?

title.jpg

文章创作于2020年4月,大约7000字,预计阅读时间18分钟,请坐和放宽。

0 - 前言

注:本文默认传统算法是所有工程师的基础技能,所以后面提到的算法主要指机器学习以及深度学习等算法。

尽管目前本人求职的重心还是在后端上,但是为了能从现在的人工智能专业硕士顺利毕业,也为了让自己顺便拓展算法工程这条路,简单的规划一下算法这部分需要补的知识和技能还是有必要的。

本文以拿到2021算法岗Offer为目标,从2020的算法岗面经入手,分析需要点的技能树都有哪些。

1 - 不同算法岗的区别

首先需要说明的一个地方就是,不同领域的技术岗位,都会按照接触科研和业务的程度来进行一定的区分。

可以简单参考知乎上霍华德在问题“学术界科研(research)与工业界研发(R&D)有什么区别?”的回答[1],算法岗可以大致分为:

  • 业务导向,大部分情况下以Development为主;
  • 技术导向,Research和Development兼顾;
  • 科研导向,大部分情况下会Research为主;

近几年这一点在算法岗上表现体现的比较明显,因为在几年前大部分算法都还只在研究阶段,而最近随着一些成熟的机器学习封装库被开放过后,部分行业里开始发现这些算法可以产生实际的价值,所以算法岗位也就呈现了近几年的爆发式增长,尤其是业务导向的算法工程师(因为大部分公司还是希望这些算法能更多更快的产出业务价值)。

当然这话其实说的已经有点晚了,现在已经不是刚开始那样的时候了,那个时候会使用框架、调调参就可以拿到Offer,现在的算法岗更像是浪潮过后的归于正常的情况,不仅需要扎实的理论基础,还需要丰富的项目实践。

我个人是更倾向于业务导向的算法工程,所以本文以这部分为目标来编写,如果你有兴趣了解三种不同岗位的细节,可以阅读夕小瑶的一篇公众号文章《拒绝跟风,谈谈几种算法岗的区别和体验》[2]。

2 - 2020面经读后感

为了更好地了解各行业公司都比较看重哪些方面的东西(很可能也都是这些公司在用的技术),我选择直接从算法岗的面经里去寻找可能的答案,面经贴主要是牛客网上的[3]。

找到的点可以简单分为以下几类:

  • 纯数学相关
  • 机器学习
  • 深度学习
  • NLP相关
  • 推荐算法

一些传统算法相关就不在此列了(Leetcode和一些书比如《剑指Offer》整理的也足够多了)。我能够看到的面经是有限的,面经里提供的内容也是有限的,所以后面的内容不能说能概括到全部,但是至少能提取出很大一部分频繁出现的关键词(如果真的有需要的话再写个爬虫+关键词提取吧)。

内容因为并没有特别多的先后依赖关系,所以就按照在面经里出现的顺序来列了。

2.1 - 纯数学相关

  • 事件概率计算
  • 狄利克雷分布
  • 最大似然估计和贝叶斯估计
  • ...

2.2 - 机器学习

  • 数据清洗、数据平滑
  • 常用的降维方式、PCA
  • LDA(Linear Discriminant Analysis)
  • 决策树,ID3、C4.5、CART
  • XGBoost、LightGBM、随机森林、Adaboost、GBDT
  • SVM原理、对偶问题
  • L1、L2正则化
  • 过拟合
  • 特征选择方法
  • LR(Logistic Regression)和SVM、Linear SVM 和 LR
  • 聚类方法、K-means、层次聚类
  • 模型的评价指标、ROC
  • 朴素贝叶斯原理
  • scikit-learn、numpy
  • bagging和boosting
  • 集成学习
  • 分类方法
  • 模型上线优化
  • 连续值、离散值,离散化连续特征的好处
  • 回归方法、线性回归、岭回归、Lasso回归、LR
  • 信息增益,信息增益比,Gini系数的关系
  • One-Hot编码的原理及意义
  • Optimizers(Gradient Descent、...)
  • 统计学习算法
  • ...

2.3 - 深度学习

  • Feedforward Neural Network
  • Back Propagation
  • Layers,convolutional、pooling、full connected
  • CNN(卷积)、RNN(梯度消失问题)、LSTM、GRU
  • GAN
  • 目标检测,R-CNN、Fast R-CNN、Faster R-CNN、YOLO、SSD、...
  • SoftMax、Sigmoid
  • Embedding
  • 注意力机制
  • GCN(Graph Convolutional Network)
  • Optimizers(Gradient Descent、BGD、SGD、Adam、Adagard...)
  • Tensorflow、Keras、PyTorch
  • Activation(sigmoid、softmax、relu...)
  • MobileNet
  • Dropout
  • CPU、GPU加速
  • ...

2.4 - NLP相关

  • 关键字提取、TF-IDF
  • 命名实体
  • LDA(Latent Dirichlet Allocation)
  • word2vec
  • Bert、Transformer
  • ...

2.5 - 推荐算法

  • 基于内容的推荐
  • 协同过滤推荐、UserCF、ItemCF
  • 如何处理稀疏矩阵
  • ...

2.6 - 面经总结

在大部分算法面试中,面试官的问题都是围绕着简历上的项目来问的,我们可以看到上面的很多项目所涉及到的点,面试官都有可能往深了问,比如:

  • SVM原始问题为什么要转化为对偶问题,为什么对偶问题就好求解,原始问题不能求解么?
  • K-means 中我想聚成100类 结果发现只能聚成98类,为什么?
  • LR和SVM这两个应用起来有什么不同?
  • 对于PCA,会有第一主成分、第二主成分,怎么为什么第一主成分是第一,原因是什么?
  • bagging 和boosting 哪个可以让结果的方差更小一些,为什么?
  • ...

所以在学习过程中不光要知道How,还是要多知道几个Why,一是为了能在面试的时候能回答出问题,二是为了更好地理解手里的这个工具。

3 - 算法的基础技能树

面经总结出来的点也还是有点乱,所以我又参考了一些算法学习路线的帖子来简单的归类梳理一下各个点,主要是参考的机器之心的这篇完备的 AI 学习路线,最详细的中英文资源整理[4],时效为2019-04-28,还参考了一个不知道我什么时候在哪里找到的知识点总结图,如果有人知道出处的话可以在评论里和我说一下,图片链接会附在文章末尾(图片很大,所以放进来会看不清)。

3.1 - 数学基础

  • 高等数学
  • 线性代数
  • 概率论与数理统计

并不是说要把上面三个教材吃的完全透了才开始学习后面的,其实人工智能领域很多方法都只是用到了其中的一小部分,有一些专门总结了的机器学习中需要使用到的数学知识的书籍和文档[4],你可以在机器之心的公众号文章找到这些(我在文章末尾也会上一个链接),具有基本的数学基础的可以用来复习,没有数学基础的还是建议在看不懂的地方回顾到教材。

有些帖子可能会在数学基础这部分加上一个凸优化,个人理解上,在纯粹的学习过程中凸优化可以说是最枯燥的一门课,里面大部分是一些凸优化的定义和理论公式的证明,所以建议在后期遇到的时候再切入某一个点深入学习。

3.2 - 编程基础

在数值分析和人工智能这方面,还是Python支持的库比较方便,在入门学习方面已经足够使用了,版本目前推荐3.5 or 3.6。

Anaconda(or Miniconda)是一个比较方便的Python虚拟环境和包管理软件,但是在某些时候会遇到麻烦事(比如一些算法框架的奇奇怪怪的环境要求),但是在大部分情况下的入门阶段已经足够使用了。

Python的IDE大部分人常用的就是Pycharm,如果有些能力折腾的,可以考虑用vscode+插件等等。

3.3 - 数据处理/分析/挖掘

实际使用中,很多机器学习、深度学习方法只有在高质量数据中才能起作用,比如数据的信息量足够多、噪声和错误信息足够少。而实际数据收集过程中,很多情况下不可能让数据这么完美,所以需要进行一些初步的数据处理(采集、清洗、采样、去噪、降维、...)。

除了Python语言基础,还需要掌握一些基础的数据处理库,比如numpy、pandas、matplotlib等,可以参考机器之心推荐的《利用python进行数据分析》。

这本书含有大量的实践案例,你将学会如何利用各种Python库(包括NumPy,Pandas、Matplotlib以及IPython等)高效地解决各式各样的数据分析问题。如果把代码都运行一次,基本上就能解决数据分析的大部分问题了。

另外还有就是[4]:

数据挖掘可以帮助我们初步的理解数据各特征之间具有的一些关系,增加或者删除一些特征来帮助后续的学习。数据挖掘可以通过一些导论书籍或者课程进行一些初步系统性的了解,其中的大部分原理都不是很高深。

3.4 - 传统机器学习

3.4.1 - 入门

如果在入门的时候,一开始就学习数学和理论公式,也不去弄明白这个东西到底有什么用,就很难去理解到底为什么需要这些理论。

在学习每个机器学习算法前,可以先笼统的明白这个东西的作用,然后带着问题“这个是怎么实现的?”去探究算法的理论,才能比较贯通的理解其中的数学和公式。

这里推荐一个网站,产品经理的人工智能学习库

人工智能领域的百科全书,非常适合小白和新手入门 AI 领域。现在市面上大家看到的绝大部分 AI 资料都是追求严谨的“理工科天书”,这个世界不缺少严谨真确晦涩难懂的 AI 资料,但是很缺容易理解的内容。我们希望抛开复杂的公式,复杂的逻辑,复杂的专用名词。做一套文科生也能看懂的 AI 知识库。

3.4.2 - 理论

机器学习的理论部分大概有:

  • 机器学习所面向的问题

    • 分类

      • 决策树
      • K-近邻
      • SVM
      • Logistic回归
      • 贝叶斯
      • 随机森林
      • ...
    • 回归

      • 线性回归
      • 最小二乘回归
      • 局部回归
      • 神经网络
      • ...
    • 聚类

      • K-means
      • EM
      • ...
    • 降维

      • 主成分分析 PCA
      • 线性判别分析 LDA
      • ...
    • ...
  • 回归

    • 线性回归
    • Logistic回归
    • ...
  • 决策树与随机森林

    • ID3
    • C4.5
    • CART
    • 回归树
    • 随机森林
    • ...
  • SVM

    • 线性可分
    • 线性不可分
  • 最大熵与EM算法
  • 多算法组合与模型优化

    • 模型选择
    • 模型状态分析
    • 模型优化
    • 模型融合
  • 贝叶斯网络
  • 隐马尔可夫链HMM

    • 马尔可夫链
    • 隐马尔可夫链
  • 主题模型LDA
  • 集成学习
  • ...

内心OS:这总结下来基本上和某些书的目录差不多了。

推荐课程[4]:

推荐书籍[4]:

  • 西瓜书《机器学习》- 周志华,主要是机器学习的核心数学理论和算法。
  • 《统计学习方法》- 李航,更加完备和专业的机器学习理论知识,作为夯实理论非常不错。
  • 《Pattern Recognition and Machine Learning》,中文译名《模式识别与机器学习》,简称PRML,出自微软剑桥研究院实验室主任 克里斯托弗·毕晓普(Christopher Bishop)之手,豆瓣评分9.5,目前这本书已经被微软开源,地址:https://www.microsoft.com/en-...,书是英文的,网上可以找到一些第三方的中文翻译,不过还是建议读英文,再次也是中英对照着来。

3.4.3 - 实践

在初步入门和学习理论后,为了活学活用学到的算法,可以尝试进行实践。

首先是一些可以拓展能力的常用工具(免得自己造轮子):

  • scikit-learn:一个Python第三方提供的非常强力的机器学习库,它包含了从数据预处理到训练模型的各个方面。在实战使用scikit-learn中可以极大的节省我们编写代码的时间以及减少我们的代码量,使我们有更多的精力去分析数据分布,调整模型和修改超参。
  • XGBoost:xgboost是大规模并行boosted tree的工具,它是目前最快最好的开源boosted tree工具包,比常见的工具包快10倍以上。在数据科学方面,有大量kaggle选手选用它进行数据挖掘比赛,其中包括两个以上kaggle比赛的夺冠方案。在工业界规模方面,xgboost的分布式版本有广泛的可移植性,支持在YARN, MPI, Sungrid Engine等各个平台上面运行,并且保留了单机并行版本的各种优化,使得它可以很好地解决于工业界规模的问题。
  • LightBGM:​ LightGBM(Light Gradient Boosting Machine)同样是一款基于决策树算法的分布式梯度提升框架。为了满足工业界缩短模型计算时间的需求,LightGBM的设计思路主要是两点:1. 减小数据对内存的使用,保证单个机器在不牺牲速度的情况下,尽可能地用上更多的数据;2. 减小通信的代价,提升多机并行时的效率,实现在计算上的线性加速。由此可见,LightGBM的设计初衷就是提供一个快速高效、低内存占用、高准确度、支持并行和大规模数据处理的数据科学工具。
  • ...

然后就可以去Kaggle上和大佬们对线了,如果你有能力也有idea,可以自己开出一个项目来做。

如果你对某些算法有更深程度的理解,你甚至可以尝试用自己代码复现这些算法。

推荐书籍:

  • 《Scikit-Learn 与 TensorFlow 机器学习使用指南》:这本书分为两大部分,第一部分介绍机器学习基础算法,每章都配备 Scikit-Learn 实操项目;第二部分介绍神经网络与深度学习,每章配备 TensorFlow 实操项目。如果只是机器学习,可先看第一部分的内容。

3.5 - 深度学习

3.5.1 - 入门

在这里同样推荐产品经理的人工智能学习库

3.5.2 - 理论

深度学习的理论部分大概有[4]:

  • 基础神经网络

    • 神经元
    • 激活函数
    • 基本结构:输入层、隐藏层、输出层
    • 反向传播算法
  • CNN

    • 卷积层
    • 池化层
    • 全连接层
    • CNN的典型网络结构(LeNet, AlexNet, VGG, ResNet, ...)
  • RNN

    • 单向RNN
    • 双向RNN
    • 深度RNN
    • LSTM
    • GRU
  • GAN
  • ...

你可以从广度上入手,在都了解的基础上,选择一个方向进行深入学习:

  • 计算机视觉(图像、视频处理,主要用CNN);
  • 自然语言处理NLP(包括文本、语音处理,序列数据往往需要RNN);
  • 生成模型(GAN、VAE等等);

推荐课程[4]:

推荐书籍[4]:

  • 开源书籍《神经网络与深度学习》 - 复旦邱锡鹏,这本书花费了邱老师三年的时间,将自己的研究,日常的教学和实践结合梳理出这个深度学习知识体系。该书主要介绍神经网络与深度学习中的基础知识、主要模型(前馈网络、卷积网络、循环网络等)以及在计算机视觉、自然语言处理等领域的应用[5]。
  • 花书《深度学习》,源:Github网友翻译,该书从浅入深介绍了基础数学知识、机器学习经验以及现阶段深度学习的理论和发展,它能帮助人工智能技术爱好者和从业人员在三位专家学者的思维带领下全方位了解深度学习。
  • 神贴《深度学习 500 问》,作者是川大的一名优秀毕业生谈继勇。该项目以深度学习面试问答形式,收集了 500 个问题和答案。内容涉及了常用的概率知识、线性代数、机器学习、深度学习、计算机视觉等热点问题,该书目前尚未完结,却已经收获了Github 2.4w stars(现在已经3.7w star了)。

3.5.3 - 实践

在初步入门和学习理论后,为了活学活用学到的深度学习算法,可以尝试进行实践。

首先是一些可以拓展能力的常用工具(免得自己造轮子):

  • TensorFlow,Google开源的深度学习框架,不过接口都比较底层,可能入门级稍难。
  • Keras,一个用 Python 编写的高级神经网络 API,它能够以 TensorFlow, CNTK, 或者 Theano 作为后端运行。Keras对入门友好,不过其中过多的封装可能会导致需要自定义修改比较麻烦,所以他们主要面向的是快速实验、快速验证的任务。
  • PyTorch,Facebook发布的一套深度学习框架,PyTorch专注于直接处理数组表达式的较低级别 API。去年它受到了大量关注,成为学术研究和需要优化自定义表达式的深度学习应用偏好的解决方案。

关于哪个工具更好的问题,"支持者"之间也是争议不断,其实也不用纠结到底应该选哪一个,都试试不就知道了(逃。

选择一个工具学会后,就可以去Kaggle上和大佬们对线了,如果你有能力也有idea,可以自己开出一个项目来做。

3.6 - 其他

至于强化学习、迁移学习、计算机视觉、NLP、推荐系统、知识图谱等内容,限于文章篇幅,就不在这里介绍了,不过你可以在机器之心的那篇文章中找到和他们有关的内容。

3.7 - 论文阅读

机器学习、深度学习大部分理论内容都来自计算机科研领域发表的论文,当下的前沿技术也都在近几年发表的论文中。

作为入门、理论、实践的之后一个拓展阶段,可以通过阅读前沿论文来增加知识面。

由于前沿论文阅读并不能算是一个业务导向的算法工程师所必须具有的能力,所以在这就不做过多的介绍了,同样,你可以在机器之心的那篇文章中找到关于阅读前沿Paper的相关介绍。

4 - 总结

不久前,某404网站给我推送了一个视频,名字看起来非常标题党,Don't learn machine learning - Daniel Bourke,源:Youtube,其中作者核心的内容是不要为了只是学习算法而学习算法,要为了创造产品(或者说应用、或者说解决问题)而学习算法,有条件的同学可以看看(暂时还没有看到国内的翻译搬运,如果有时间有机会的话我就翻译搬运一下吧)。

面向Offer学习未必是最优的一条路。我的目标是以后端为主线发展,之所以还没有完全的放弃这部分的算法,一部分是因为我的专业,更多的原因是我知道在某些问题上只有这些算法才能有效地解决,会用更多的算法也可以让程序员解决更多的问题。

5 - 参考文章

查看原文

赞 1 收藏 1 评论 0

Yumiku 赞了文章 · 2020-04-10

libcsp: 一个 10 倍于 Golang 的高性能 C 语言并发库

libcsp是一个C语言实现的基于CSP模型的高性能并发库, 利用它你可以用C开发一些高性能项目.

特性:

  • 支持多核
  • 高性能调度器
  • 编译时栈大小静态分析
  • 高性能 Lock-free 通道
  • 支持 netpoll 和 timer

Golang和Libcsp比较

// Golang                                       // Libcsp
go foo(arg1, arg2, arg3)                        async(foo(arg1, arg2, arg3));
                                              
var wg sync.WaitGroup                           sync(foo(); bar());
wg.Add(2)                                            
go func() { defer wg.Done(); foo(); }()
go func() { defer wg.Done(); bar(); }()
wg.Wait()

runtime.Gosched()                               yield();
                                              
chn := make(chan int, 1 << 6)                   chan_t(int) *chn = chan_new(int)(6);
num = <-chn                                     chan_pop(chn, &num);
chn <- num                                      chan_push(chn, num);
                                              
timer := time.AfterFunc(time.Second, foo)       timer_t timer = timer_after(timer_second, foo());
timer.Stop()                                    timer_cancel(timer);

Github: https://github.com/shiyanhui/libcsp
文档: https://libcsp.com

查看原文

赞 7 收藏 6 评论 0

Yumiku 关注了专栏 · 2020-04-10

前端早早聊

技术经用得上、听得懂、抄得走,加微 codingdreamer 围观

关注 600

Yumiku 赞了文章 · 2020-04-10

职业思考:技术和管理路线之间如何取舍

著作权归作者所有。商业转载请联系 Scott 获得授权,非商业转载请注明出处[务必保留全文,勿做删减]。

程序员的青春饭,有人说是 30 岁,有人说是 35 岁,现实是这样么?真写不动的时候,要不要转,什么时候转,怎么转?

2018 年,刚好工作 8 年,也是我而立之年,在 30 岁这一年我从技术研发全面的转向了技术管理,经历了一个夜夜深思夜夜失眠的阶段,今天把我的思考结果分享给大家,包括身边的一些人一些事也会贯穿在本文之中,尝试给大家梳理一些选择之前需要考量的因素。

在开始之前,先送大家四句大白话:

  • 了解了解不停去了解
  • 规划规划不断做规划
  • 执行执行绝对要执行
  • 回馈回馈坚持做回馈

其实按照池老师的处事三原则就是:闭环、谁难受谁推进、Think Bigger,换做是我自己的方式,总结一下,就是不断去了解自己,不断去修正规划,不断的推动执行,不断上下左右回馈,如此反复再无它。

做技术到底是做什么

未来机器会取代我们,就像年轻的工程师会取代我们一样,凭借更佳的脑力运算凭借更低的应用成本,那我们当下做技术到底是做什么?

工程师赖以生存的技术,是驱动这个世界发生质变的核心推力引擎,它的本质是创新也就是再造,通过消耗我们的脑力去更好的消耗计算机的算力,脑力换算力成立的前提是脑力可以做更多意识层面逻辑性的情感性的决策,可以通过强大的抽象能力分析事物的本质,而算力目前还很难做到,等到机器可以做深度抽象做自主决策的时候,就是脑力编程下岗的时候,那时候就要依靠我们人类无穷无尽的想象力与创造力来输出规则给机器,机器帮我们做最优决策和过程实现。

回到当下,最优决策和过程实现还必须靠我们这一代人,而做技术就是在做决策和付诸实现,只不过我们凭借特定的语法规则给机器编写特定的执行任务,而语法掌握的熟悉度、技术运用的熟练度以及对机器和规则运行作用过程的理解程度决定了我们的技术实力是处在哪个段位哪个 Level。

Java 的语法有它特定的结构,PHP/Python/Javascript 统统不例外,这些特定的语法规则和结构我们都可以通过记忆烂熟于心,而它们背后的技术思想,基于它们衍生的特定框架,结合它们所形成的特定语言工具生态,对于这些的熟悉程度又进一步决定了我们运用技术的灵活程度,至于对机器和规则的理解则取决于我们训练大脑的深度和广度。

看到这里,我们再来看技术二字,就很容易明白它无非就是一些技巧规则的运用,而我们常年累月所浸泡的技术体系里面,经常面对的恰恰就是这些技巧和规则,编程经历越久,对于技巧掌握的越多,对于规则理解的越深刻,基于技巧和规则我们能衍生出的再造能力也更强,而通向更强再造能力的过程中,我们做技术的本质就是不断训练自己的大脑熟悉这些技术体系,建立新的认知体系,再创造出新的解决思路和应用方法论,所以到最底层的时候,比拼的还是想象力和创造力,只是很多时候我们空有想象力和创造力,却缺少让想法落地的技巧和规则运用能力,原因在于我们训练的还不够深刻,还不够丰富,也就是量变不够充分,质变也就很难发生,做技术本质就是 架构、编程和运维保障,架构就是训练出来的脑力逻辑性,编程就是规则和技巧,而运维保障则是规则应用的土壤,把它们拆解一下,就是下图上的一些技能点:

image.png

至于如何通过技术训练我们的大脑,实际上就是如何保持技术的快速成长和持续成长,我们前文有过相关的讨论,在这里我强调一点,技术需要持续性的时间投入来训练才能保持一定速度的成长,一旦投入中断,或者投入强度变小,很快技术成长就会降速,甚至没踩上行业趋势的话,会被同行甩在后面,作为这个小话题的结尾,我举一个身边的案例:

有一个同学 13 年毕业,进入大公司历练 2 年,技术趋于资深,后来高薪跳槽到一家创业公司成为核心骨干,创业公司发展迅速,1 年内就有了将近 10 人的前端团队,就任命这个同学成为技术 TL 带领团队,但快速膨胀的业务线迅速吃满了该同学的编程和架构时间,业务越来越多团队越来越大,他也就半推白就的走向了管理岗位,2015~2016 年起 React 进入蓬勃发展的时期,也是 Node 开始大规模应用的时期,他身不由己的松懈了对于技术深度的精进,到 2018 年时虽然薪资拿的较高,但原有的技术体系与当下的技术栈已不合时宜,编程的综合能力也快速下降,更重要的是,他内心依然向往编程,对于技术管理并没有建立太浓厚的兴趣也没有积淀太多成熟的管理方法论,后来痛定思痛,跳槽出来,降薪 30% 进入了一家公司做基础架构,技术重新捡起,降薪是因为他的技术能力已经低于市面行情所需求的实力,从 18 年到 19 年,他越来越开心,虽然薪资还没当年拿的多,但技术总算慢慢追赶上来了,所以,技术的精进需要持续的投入,如果中断就很快掉队,在切向管理的时候,一定要深思熟虑三思后行。

做管理到底是做什么

技术是以熟练的技巧和规则为基础,靠兴趣、想象力和创造力驱动,那技术管理是做什么呢,把管理丢开只做技术可不可以?答案是可以,答案同时也是不可以,可以还是不可以要分阶段来看,在职业的早期阶段是可以的,在走向资深的阶段后就不可以,因为管理不仅仅是以人为对象,包括自己包括组织,包括一件件复杂的事情和合作关系,都需要具备管理能力,为什么需要管理能力,是因为技术编程现在已经进入了深水区,光具备 编程、架构和运维实际上只是具备了基础能力而已:

image.png

抛开基础技能还需要更多其他的综合能力,假如业务走在最正确的方向,确定最合理正确的指标,运营收集到最客观真实的客户反馈,产品规划出最直击用户心灵最命中业务痛点的产品路线,产出最完整最闭环最精准的 PRD 文档,UED 同学设计出最符合用户需求最契合 PRD 文档的设计稿,服务端提供最稳定最明确的接口稳定和接口联调环境,项目经理把握最稳妥最可控的迭代节奏...在这样的环境下,有可能你能专心的写代码不需要跟各方有太多纠缠,可是试问下:这个 “最” 字有几个人可以做到,你经历过这样超高命中率超强配合能力的团队么?任何一个环节出岔子,就可能代码编程实现中的重大干扰因素,这还仅仅是对于一个人是如此,如果 10 个人前端团队,每个人都处在这种不稳定的状况下呢?

其实我们都处在一个个不完美的公司,一个个不完美的团队中,即便如 BAT AMD,又何尝没有道不完的心酸,团队越大问题越复杂,而作为技术管理者,职责就是解决团队中出现一切问题,并能促使团队不断的变得更优秀,前文我们讨论如何带领小团队时有张截图,这是小菜前端早期的一个阶段:

image.png

其实一个团队出问题的时候,是远不止上面这些问题的,当这些同时出现交叉出现的时候,团队的技术管理者应该做什么呢?答案就是发现问题、定义问题、制定方案、解决问题、复盘总结,发现问题是最容易做的事情,团队中大家的吐槽随时都是发现了问题,但定义问题则是一个更重要的能力,如何准确的定义问题直接决定了能否制定出正确的应对方案。

我们再把上面的其他综合能力做个分类归纳,会发现如下特征:

image.png

所有的这些能力集合,都可以成为管理岗需要的能力,只不过我们所认为是管理是比较狭义的管理能力而已,它的对象更多在于组织内部的向上向下和向左向右。

这时候我们也就意识到,管理所涵盖的的确不仅是事情,而是将所有能力综合后完整的推进路径,简言之就是驱动团队的方法论,方法论是可以训练的,但这些方法论往往跟编程本身没有关系,方法论投入精力越多,编程参与就越少,这就是技术向和管理向的天然矛盾,但方法论的背后反而有很多通用套路是对编程也是有益的,比如 PDCA 这样的做事规划模型,我对它做了改进,加入了意愿和能力的维度:

image.png

尽然技术路线与管理路线有矛盾也有统一,但最终它们通往的方向不同,我们在做职业转型的时候,就必然要取舍,就必然会经历内心的煎熬,就必然在某条路上愈行愈远。

管理能力一定是加分项

看完前面两个论述,我们会觉得管理好像不是一件很容易做的事,也不太像是一个对很工程师生涯很友好的选型,事实上,在整个市场的技术岗位稀缺度上,技术管理者与技术专家是同样的稀缺,无论这个同学技术是一般般还是非常好,身上具备管理能力一定是加分项。

因为技术团队是最好带也是最难带的,好带是因为技术同学的单纯与淳朴,难带是技术同学的天真和不成熟,一个好的技术管理者可以把团队打理的井井有条,发挥更大的效能,让业务方爽的不要不要的,团队成员也都能不断的获得成长,而一个不合格的管理者则会把团队搞的乌烟瘴气、怨声载道,甚至跟业务方无法建立信任关系,这对于一家企业是灾难性的后果。

因此用人方任命技术管理者,宁肯不求有多完美的果,却希望不要有太多的过,因为一旦技术团队与公司之间撕裂或者内部撕裂,整个公司将付出更巨大的资本和时间成本,来再次把团队校正到正确轨道,而由于技术团队是公司最贵重的资源,这个技术管理岗就至关重要,这就解释了为什么它是加分项。

最后一个观点,技术管理一定离不开技术二字,一个管理者可以不再精通技术,但要对技术有充分的经验积累和敏感度,要具备比较成熟的技术评估和技术人才运用的方法论,一定不可以抛开技术,纯粹从业务和人的视角做管理,比如用强 KPI 强指标的考核,或者只追求短期业务实现而不顾及长期基础设施建设和技术栈的迭代升级,那样对于技术团队的长期人才体系会有很大的伤害。

技术生涯跟天赋强相关

技术天赋越好的人,技术价值最大化的成功率越高,在技术的圈子里,技术成就能有多大,的确是因人而异,是有天赋因素加成的。

其实十几年看下来,真正在纯技术领域走向成功的人并不少,但前端这个领域却凤毛麟角,通常达到阿里的标准 P7 专家水平,就已经是大部分人的职业天花板了,还有少部分人能继续攀登到 P8 高级技术专家的高度,再往上走,就往往跟技术编程本身关系不大了,这也是为什么大部分人到了 P7,年龄也大都接近 30 岁上下,会慢慢转向管理,一个是市场需求大,一个是自己的职业天花板在这时候就遇到了,毕竟背后付出的心血都是脑力和身体。

所以,如果我们发现自己的技术天赋很高,那么可以尽量迟一点考虑管理,如果技术天赋一般,那么可以早一点理解管理的意义,并做一些必要的准备,将来迎接这个转型。

什么时候该考虑做管理

往往你做管理的时候,会源于一些外力,比如你跳槽去了一家新公司,刚好有这个管理岗坑位,比如你在本岗位干的风生水起,老板比较挺你让你带俩人试试,比如公司组织架构调整,你是被动任命,比如你所在团队老板离职,你临危受命等等,很多时候你从技术开始转向管理,都有一些机缘巧合,不知不觉就半只脚迈进了管理的池子中。

而你身上一定有了管理的标签,你就不再是一个人走,而是带着一群人走,你不再是对自己负责,而是对大家负责,你不再是仅仅面对业务,更是面向组织,可能就是这一夜之间,你的使命已经发生了根本性的跃迁,可是往往你要过很久才能慢慢明白过来。

这样的管理,其实是大部分人遇到的,也就是被动式的考虑,被动式的征召,而转向了管理,其实最好的管理路线,其实也是最好的技术路线,都跟我们前面提到的能力和意愿强相关,我们意愿上愿意转管理喜欢做管理,那么可以切换,如果意愿上极度排斥,那么就要深思而行,那怎么判断自己是喜欢还是排斥呢,我拿几张表格给大家测试下,看你喜不喜欢做这样的事情?

是否喜欢推导人的做事动机,是否愿意用这样的图来复盘自己和他人的项目过程,看所有人的能力和意愿处在哪个区间:

image.png

是否对商业模式感兴趣,愿意花时间研究不同行业不同公司不同团队,他们的成长情况,包括自己团队的成长阶段:

image.png

是否喜欢对问题进行分析归类,从人、事、组织等多个角度来抽象问题本质,并且有足够的热情去推动解决:

image.png

是否对于管理的手段,管理者的定位,以及所有公司决策层下发流程的执行力度有自己的分析,有更客观的评估:

image.png

是否真的愿意花时间去理解使命愿景价值观,人的道德品质和内心价值取向这些务虚的东西,是否认同人的驱动和需求层次与管理者胸怀及成就他人的关系:

image.png

以上这些事情与编程几乎无关,大家如果毫不排斥甚至喜欢,那么我认为当你的技术到达了技术专家的时候,或者再晚一点,都是可以考虑转管理的时候了,如果继续走技术路线,那么相信这些感兴趣的分析也会帮你更好更客观的分析所处的环境与所能发挥的价值。

小结

技术研发和技术管理有很多不同,但并不冲突,甚至在一个人的职业生涯中,二者的定位和比重也会来回切换,那是因为技术总是在急速的变革之中,只有积极的跟进技术浪潮,对技术保持较高的好奇心和敏感度,才能更好地做技术管理,同理在一直追求技术造诣和工程师价值的路上,也要时不时培养训练一些基础性的管理能力,至少是管理意识,这些软实力可以成为你当前岗位内推进各种疑难事项的润滑剂,同时也为将来的另一个职业路线打下一些基础,总之,技术路线与工程师创新价值的长远实现是贴合的,而技术管理与商业、团队和组织能力成长更贴合,两者在少数人身上可以很好的整合,形成软硬兼具能虚能实的综合竞争力,而我们大多数人,可以让自己在两个方面都不要有明显短板,才能在长期的职业生涯中,总能收获期许的回报。

Scott 近年面试或线下线上技术分享,遇到太多前端同学,由于团队原因/个人原因/职业成长/技术与管理通道,甚至家庭城市等等原因,在理想国与现实之间,在放弃与坚守之间,摇摆不停,心酸硬扛,大家可以找我聊聊南聊聊北,对工程师的宿命和价值有更多的看见与了解,Scott 微信: codingdream,也可以来关注 Scott 跟进我的动态

2.png
1.png

查看原文

赞 6 收藏 4 评论 0

Yumiku 收藏了文章 · 2020-04-08

为什么要用Go语言?

本文章创作于2020年4月,大约6000字,预计阅读时间15分钟,请坐和放宽。

logo.png

前言

Go 是一个开源的编程语言,它能让构造简单、可靠且高效的软件变得容易[1]。

Go 语言被设计成一门应用于搭载 Web 服务器,存储集群或类似用途的巨型中央服务器的系统编程语言。对于高性能分布式系统领域而言,Go语言无疑比大多数其它语言有着更高的开发效率。它提供了海量并行的支持,这对于游戏服务端的开发而言是再好不过了[1]。

其实早在2018年前,我就已经有在国内的程序员环境中断断续续地听到Go语言的消息,Go语言提供的方便的并发编程方式,十分适合我当时选择的毕业设计选题,但是受限于导师的语言选择、项目的进度追赶、考研的时间压榨,一直没有机会来好好地学习这门语言。

在进入研究生阶段后,尽管研究的方向和算法相关,但未来的职业方向还是选择了以后端为主,主要是因为想做更多和业务相关的工作。为了能在有限的时间里给予自己足够深的知识底蕴,选择了一些让自己去深入了解的方向,Go语言自然也在其中,今天终于有机会来开始研究这门语言。

为什么要用Go语言?

撰写此文的初衷,是本文的标题,也是我作为初学者一直以来的疑问:

“我为什么要用Go语言?”

为了回答这个问题,我翻阅了很多Go语言相关的文档、书籍和教程,我发现我很难在它们之中找到非常明显直接的答案,书上和教程只会说,“是的,Go语言好用”

对于部分人来说,这个问题的答案或许很“明显”,比如选择Go语言是因为Google设计的语言、Go开发赚的钱多、XX公司使用Go语言等等,如果想要了解这门语言更加本质的东西,仅仅这些答案我认为是还不够的。

部分Go的教徒可能会说,他们选择的理由是和语言本身相关的,比如:

  • Go编译快
  • Go执行快
  • Go并发编程方便
  • Go有垃圾回收(Garbage Collection, GC)

的确,Go是有这些特点,但这并非都是Go独有的

  • 运行时解释的脚本语言(比如Python)几乎不需要时间编译
  • C、C++甚至是汇编,基本上能够榨干一台机器的大部分性能
  • 大部分语言都有并发编程的支持库
  • 大部分语言都不需要程序员主动关注内存情况

一些Go的忠实粉丝把这种All in One的特性作为评价语言的标准,他们认为至少在这些方面,Go是可以完美的代替其他语言的。

那么,Go真的能优秀到完全替代另一个语言么?

其实未必,我始终认为银弹是不存在的[2],无论是在这次调查前,还是在这次调查后。

本文从Go语言被设计的初衷出发,深入互联网各种角落,调查Go所具有的那些特性是否足够优秀,同时和其他语言进行适当的比较,你可以选择性的阅读、接受或者反对我的内容,毕竟有交流才能传播知识。

我的最终目的是让更多的初学者看到Go没有轻易暴露出的缺点,同时也能看到Go真正优秀的地方

设计Go的初衷

Go语言的主要目标是将静态语言的安全性和高效性与动态语言的易开发性进行有机结合,达到完美平衡,从而使编程变得更加有乐趣,而不是在艰难抉择中痛苦前行[3]。

Google公司不可能无缘无故地设计一个新语言(一些特性相比于其他语言也没有新到哪里去),这一切肯定是有原因的。

设计Go语言是为了解决当时Google开发遇到的一些问题[4]:

  • C++编译慢、没有现代化(入门级友好的)的内存管理
  • 数以万计行的代码,难以维护
  • 部署的平台各式各样,交叉编译困难
  • ......

joke.png

找不到什么合适的语言,想着反正都是弄来自己用,Google选择造个轮子试试。

Go 语言起源 2007 年,并于 2009 年正式对外发布。它从 2009 年 9 月 21 日开始作为谷歌公司 20%兼职项目,即相关员工利用 20% 的空余时间来参与 Go 语言的研发工作。该项目的三位领导者均是著名的 IT 工程师:Robert Griesemer,参与开发 Java HotSpot 虚拟机;Rob Pike,Go 语言项目总负责人,贝尔实验室 Unix 团队成员,参与的项目包括 Plan 9,Inferno 操作系统和 Limbo 编程语言;Ken Thompson,贝尔实验室 Unix 团队成员,C 语言、Unix 和 Plan 9 的创始人之一,与 Rob Pike 共同开发了 UTF-8 字符集规范。自 2008 年 1 月起,Ken Thompson 就开始研发一款以 C 语言为目标结果的编译器来拓展 Go 语言的设计思想[3]。

go-designers.png

Go 语言设计者:Griesemer、Thompson 和 Pike [3]

当时Google的很多工程师是用的都是C/C++,所以语法的设计上接近于C,Go的设计师们想要解决其他语言使用中的缺点,但是仍保留他们的优点[5]:

  • 静态类型和运行时效率
  • 可读性和易用性
  • 高性能的网络和多进程
  • ...

emmm,这些听起来还是比较玄乎,毕竟设计归设计,实现归实现,我们回顾一下现在Go的几个主要特点,编译速度、执行速度、内存管理以及并发编程。

Go的编译为什么快

当然,设计Go语言也不是完全从零开始,最初Go的团队尝试设计实现一个Go语言的编译前端,由基于C的gcc编译器来编译成机器代码,这个面向gcc的前端编译器也就是目前的Go编译器之一的gccgo。

与其说Go的编译为什么快,不如先说说C++的编译为什么慢,C++也可以用gcc编译,编译速度的大部分差异很有可能来源于语言设计本身。

在讨论问题之前,其中需要先说明的一点是:这里比较的编译速度都是在静态编译下的

静态编译和动态编译的区别:

  • 静态编译:编译器在编译可执行文件时,要把使用到的链接库提取出来,链接打包进可执行文件中,编译结果只有一个可执行文件。
  • 动态编译:可执行文件需要附带独立的库文件,不打包库到可执行文件中,减少可执行文件体积,在执行的时候再调用库即可。

两种方式有各自的优点和缺点,前者不需要去管理不同版本库的兼容性问题,后者可以减少内存和存储的占用(因为可以让不同程序共享同一个库),两种方式孰优孰弱,要对应到具体的工程问题上,Go默认的编译方式是静态编译

回到我们要讨论的问题:C++的编译为什么慢?

C++编译慢的主要两个大头原因[6]

  • 头文件的include方式
  • 模板的编译

C++使用include方式引用头文件,会让需要编译的代码有乘数级的增加,例如当同一个头文件被同一个项目下的N个文件include时,编译器会将头文件引入到每一份代码中,所以同一个头文件会被编译N次(这在大多数时候都是不必要的);C++使用的模板是为了支持泛型编程,在编写对不同类型的泛型函数时,可以提供很大的便利,但是这对于编译器来说,会增加非常多不必要的编译负担。

当然C++对这两个问题有很多后续的优化方法,但是这对于很多开发者来说,他们不想在这上面有过多时间和精力开销。

大部分后来的编程语言在引入文件的方式上,使用了import module来代替include 头文件的方式,import解决了重复编译的问题,当然Go也是使用的import方式;在模板的编译问题上,由于Go在设计理念上遵循从简入手,所以没有将泛函编程纳入到设计框架中,所以天生的没有模版编译带来的时间开销(没有泛型支持也是很多人不满Go语言的理由)。

在Go 的1.5 版本中,Go团队使用Go语言来编写Go语言的编译器(也叫自举),相比于gccgo来说:

  • 提高了编译速度,但执行速度略有下降(性能细节优化还不如gcc)
  • 增加了可编译的平台类型(以往受限于gcc)

在此之外,Go语言语法中的关键字也是非常少的(Go1.11版本里只有25个)[7],这也可以减少编译器花费在语法解析上的时间开销。

keywords.png

所以在我看来,Go编译速度快,主要出于四个原因

  • 使用了import的引用管理方式;
  • 没有模板的编译负担;
  • 1.5版本后的自举编译器优化;
  • 更少的关键字。

所以为了加快编译速度、放弃C++而转入Go的同时,也要考虑一下是否要放弃泛型编程的优点。

注:泛型可能在Go 2版本获得支持。

Go的实际性能如何

Go的执行速度,可以参考一个语言性能测试数据网站 —— The Computer Language Benchmarks Game[8]。

这个网站在不同的算法上对每个语言进行测试,然后给出时间和内存上的开销数据比对。

比较的语言有C++、Java、Python。

首先是时间开销:

time-cost.png

注意时间开销的单位是s,并且Y轴为了方便进行不同跨度上的比较,所以选取的是对数轴(即非线性轴,为1-10-100-1000的比较跨度)。

然后是内存开销:

mem-cost.png

注意Y轴为了方便进行不同跨度上的比较,所以选取的是对数轴(即非线性轴,为1000-10000-100000-1000000的比较跨度)。

需要注意的是,语言本身的性能只决定了一个程序的最高理论性能,程序具体的性能还要取决于这个程序的实现方法,所以当各个语言的性能并没有太大的差异时,性能往往只取决于程序实现的方式。

通过两个图的数据可以分析:

  • Go虽然还无法达到C++那样的极致性能,但是在大部分情况下已经很接近了
  • Go和Java在算法的时间开销上难分伯仲,但在内存的开销上Java就要高得多了;
  • Go在上述的绝大部分情况下,至少时间和内存开销都比Python要优秀得多;

Go的并发编程

Go的并发之所以比较受欢迎,网络上的很多内容集中在几个方面:

  • 天生并发的设计
  • 轻量化的并发编程方式
  • 较高的并发性能
  • 轻量级线程Goroutines、并发通信Channels以及其他便捷的并发同步控制工具

由于Go在设计的时候就考虑到了并发的支持,或者说很多特性都是为了并发而设计,这和一些后期库支持并发和第三方库支持并发的语言不同。

所以Go的并发到底有多方便?在Go中使用并发,只需要在普通的函数执行前加上一个go关键字,就可以新建一个线程让函数在其中执行:

func main() {
    go loop() // 启动一个goroutine
    loop()
}

这样带来的好处不仅仅是让并发编程更方便了,在一些特定情况下,比如Go引用一些使用了并发的库时,这些库所使用的并发也是基于Go本身的并发设计,不会存在库使用另一套并发实现的情况,这样Go调度器在处理程序中的各种并发线程时,可以有更加统一化的管理方式。

不过Go的并发对于程序的实现要求还是比较高的,在使用一些通信Channel的场合,稍有疏忽就可能出现死锁的问题,比如:

fatal error: all goroutines are asleep - deadlock!

Go的并发量可以比大部分语言里普通的线程实现要高,这受益于轻量级的Goroutine,轻量化主要是它所占用的空间要小得多,例如64位环境下的JVM,它会默认固定为每个线程分配1MB的线程栈空间,而Goroutines大概只有4-8KB,之后再按需分配。足够轻量化的线程在相同的内存下也就可以有更高并发量(服务器CPU还没有饱和的情况下),同时也可以减少很多上下文切换的时间开销[9]。但是如果你的每个线程占用空间都非常大时(比如10MB,当然这是非常规需求的情况下),Go的轻量化优势就没有那么明显了。

Go在并发上的优点很明显,也是Go的功能目标,从语言设计上支持了并发,提供了统一便捷的工具,复杂的并发业务也需要在Go的一整套并发规范体系下进行编程,当然这肯定会牺牲部分实现自由度,但可以获得性能的提高和维护成本的下降。

PS:关于Go调度器的内容在这里并没有被提及,因为很难用简单的文字向读者说明该调度方式和其他调度方式的优劣,将在未来的某一篇中会细致地介绍Go调度器的内容。

Go的垃圾回收

垃圾回收(英语:Garbage Collection,缩写为GC),在计算机科学中是一种自动的存储器管理机制。当一个计算机上的动态存储器不再需要时,就应该予以释放,以让出存储器,这种存储器资源管理,称为垃圾回收。垃圾回收器可以让程序员减轻许多负担,也减少程序员犯错的机会[10]。

在使用Go或者其他支持GC的语言时,不用再像C++一样,手动地去释放不需要的变量占用的内容空间(free/delete)

的确,这很方便(对于懒人和容易忘记主动释放的人),但是也多了一些限制(暗箱操作的不透明性以及在GC处理上的性能开销)。GC也不是万能的,当遇到一些对性能要求较高的场景,还是需要记得进行一些主动释放或优化操作(比如说自定义内存池)。

PS:将在未来的某一篇中会细致地介绍Go垃圾回收的细节(如果你们也觉得有必要的话)。

什么时候可以选择Go?

Go有很多优点,编译快、性能好、天生并发以及垃圾回收,很多比较有特色的内容也还没有说到(比如gofmt)。

Go语言也有很多缺点,比如第三方库支持还不够多(相比于Python来说就少的太多了)、支持编译的平台还不够广、还有被称为噩梦的依赖版本管理(已经在改善了,但是还没有达到完全可靠的程度)。

所以到底Go适合做什么,不适合做什么?

分析了这么多后,这个问题其实很难回答,但我们可以选择先从不适合的领域把Go剔除掉,看看我们会剩下什么。

Go不适合做什么

  • 极致高性能优化的场景,你可能需要使用C/C++,甚至是汇编;
  • 简单流程的脚本工具、数值分析、深度学习,可能Python更适合(至少目前是);
  • 搭一个博客或网站,PHP何尝不是天下第一的语言呢;
  • 如果你想比较方便找到一份的后端工作,绝大部分公司的Java岗一直缺人(在实际生产过程中,目前Go仍没有比Java表现得好太多,至少没有好到让一个部门/公司将核心业务重新转向Go来进行重构);
  • ...

你可以找到类似上面那样的很多场景,你可能会发现Go并不能那么完美地替代掉谁。

Go适合做什么

最后,到了我们的终极问题,Go到底适合做什么?

读到这里你可能会觉得,好像是我把Go的特性吹了一遍,然后突然告诉你可能Go不适合你。

Go天生并发,面向并发,所以Go的定位一直很清楚,从最浅显的视角来看,至少Go作为一个有较高性能的并发后端来说,是具有非常大的诱惑力的。

尤其对于后端相关的程序员而言,在某些业务功能的初步实现上,简洁的语法、内置的并发、快速的编译,都可以让你更加高效快速地完成任务(前提是Go的内容足以完成你的任务),不用再去担忧编译优化和内存回收、不用担心过多的时间和内存开销、不用担心不同版本库之间的冲突(静态编译)以及不用担心交叉编译平台适配问题。

大部分情况下,编写一个服务,你只需要:实现、编译、部署、运行

高效快速,足够敏捷,这在企业的绝大部分项目的初期都是适用的,这也是大部分项目对开发初期的要求。当一个项目或者服务真的可以发展下去,需求的确触碰到Go的天花板时,再考虑使用更加好的语言或方法去优化也为时不晚。

简而言之,尽管Go的过于简洁带来了很多问题(有些人说的难听点叫过于简单),Go所具有的优点,可以让大部分人用编程语言这种工具,来解决对他们而言更加重要的问题。

Go语言不是银弹,但它的确能有效地解决这些问题。

参考文章

扩展阅读

在调查Go的过程中,发现了一些比较有意思、或者比较实用的文章,一并附在这里。

  • 我为什么选择使用 Go 语言?,该文写于2016年,在我的文章基本构思完成的时候,偶然看到了这篇文章,作者有很多早期Go版本的开发经验,里面有更多的细节都是出自于工程师的经验之谈,我发现其中的部分想法和我不谋而合,你可以把这篇文章当作本文的后续扩展阅读,不过要注意文章的时效,可能提及到的一些Go的缺点现在已经被改进了。
  • C/C++编译器的工作过程,主要是供不熟悉C系的朋友了解一下编译器的工作过程。
  • The Computer Language Benchmarks Game,一个对各个语言进行性能测试的网站,里面的算法具有一定的代表性,但是不能代表所有工程可能遇到的情况,仅供参考。
  • 为什么 Go 语言在某些方面的性能还不如 Java?,这是知乎上一个2017年开始有的问题,你可以看到很多人对于这个问题的分析,从多个角度来理解语言之间的性能差异。
  • go-wiki WhyGo,Go的Github仓库上维护的Wiki中,有一篇关于WhyGo的文章整理,不过大部分是英文,里面主要是很多关于“为什么我要选择Go”的软硬稿。
  • 为什么要使用Go语言,Go语言的优势在哪里,这个知乎的提问更早,是来自2013年的Yvonne YU用户,在Go的早期其实是具有很大的争议的,你可以看到大家在各个问题上的博弈。
  • 哪些公司在使用Go,Go的Github仓库上维护的Wiki中,有一篇关于全球都有哪些公司在使用Go,不过提供的信息大部分只有一个公司名,比如国内有阿里巴巴(而人家大部分都招Java),可以看看但参考性不大。
  • Go 语言的优点,缺点和令人厌恶的设计,这是Go语言中文网上一篇2018年的文章,如果你对语言本身的一些特性的设计感兴趣,你可以选择看看,作者从很多语法层面上介绍了Go的优点和缺点。
  • Ruby China - 瞎扯淡 真的没必要浪费心思在 Go 语言上,这是我无意中找到的一篇有名的帖子,这个问题始于2013年,在Ruby China上,其中也是大佬们(可能)从各个角度来辩论Go是否值得学习,可以当作武侠小说观看。
  • The way to Go - 3.8 Go性能说明,《The way to Go》这本书上为数不多关于Go性能问题的说明。
  • C++开发转向go开发是否是一个好的发展方向?,2014年知乎上关于C++和Go的一个讨论,其实我觉得“如果选择一个并不意味着就要放弃另一个”,程序员不是研究语言的,也不应该是只靠某一门语言吃饭。
  • 我为什么放弃Go语言 Liigo,嗯,2014年,仍旧是Go争议很大的时候,CSDN上一篇阅读数很高的文章,作者从自己的角度对Go进行批判(Go早期的确是有不少问题),你可以看到早期Go的很多问题,也可以斟酌这些问题对你是否重要以及到底在2020年的Go中有没有被解决。
  • Golang 本身是用什么语言写的?,一个关于编译的有趣的问题,可以适当了解。
  • 搞懂Go垃圾回收,一篇还算比较新的分析Go垃圾回收问题的文章。
  • 有趣的编程语言:Go 语言的启动时间是 C 语言的 300 多倍,C# 的关键字最多,这篇InfoQ文章其实算是一个典型的标题党,主要使用的是一个Github上关于各个语言HelloWorld程序启动时间的测试数据(https://github.com/bdrung/sta...,使用gccgo编译的Go程序的启动时间非常地长,的确是C的300多倍,但使用GC编译的Go程序启动时间只是C的2倍。
  • Go 语言的历史回顾,我一直在寻找一个整理Go的版本变动细节的文章,在Go的官方文档和各种书籍上寻找无果时,在InfoQ上找到了一篇还算跟踪地比较新的(Go 1.0 - Go 1.13)文章,对于初学者而言,知道语言的变化也是很重要的(比如方便的知道哪些问题解决了,哪些还没有被解决),可能之后会拓展性的写一篇关于这个的文章。
查看原文

Yumiku 发布了文章 · 2020-04-07

为什么要用Go语言?

本文章创作于2020年4月,大约6000字,预计阅读时间15分钟,请坐和放宽。

logo.png

前言

Go 是一个开源的编程语言,它能让构造简单、可靠且高效的软件变得容易[1]。

Go 语言被设计成一门应用于搭载 Web 服务器,存储集群或类似用途的巨型中央服务器的系统编程语言。对于高性能分布式系统领域而言,Go语言无疑比大多数其它语言有着更高的开发效率。它提供了海量并行的支持,这对于游戏服务端的开发而言是再好不过了[1]。

其实早在2018年前,我就已经有在国内的程序员环境中断断续续地听到Go语言的消息,Go语言提供的方便的并发编程方式,十分适合我当时选择的毕业设计选题,但是受限于导师的语言选择、项目的进度追赶、考研的时间压榨,一直没有机会来好好地学习这门语言。

在进入研究生阶段后,尽管研究的方向和算法相关,但未来的职业方向还是选择了以后端为主,主要是因为想做更多和业务相关的工作。为了能在有限的时间里给予自己足够深的知识底蕴,选择了一些让自己去深入了解的方向,Go语言自然也在其中,今天终于有机会来开始研究这门语言。

为什么要用Go语言?

撰写此文的初衷,是本文的标题,也是我作为初学者一直以来的疑问:

“我为什么要用Go语言?”

为了回答这个问题,我翻阅了很多Go语言相关的文档、书籍和教程,我发现我很难在它们之中找到非常明显直接的答案,书上和教程只会说,“是的,Go语言好用”

对于部分人来说,这个问题的答案或许很“明显”,比如选择Go语言是因为Google设计的语言、Go开发赚的钱多、XX公司使用Go语言等等,如果想要了解这门语言更加本质的东西,仅仅这些答案我认为是还不够的。

部分Go的教徒可能会说,他们选择的理由是和语言本身相关的,比如:

  • Go编译快
  • Go执行快
  • Go并发编程方便
  • Go有垃圾回收(Garbage Collection, GC)

的确,Go是有这些特点,但这并非都是Go独有的

  • 运行时解释的脚本语言(比如Python)几乎不需要时间编译
  • C、C++甚至是汇编,基本上能够榨干一台机器的大部分性能
  • 大部分语言都有并发编程的支持库
  • 大部分语言都不需要程序员主动关注内存情况

一些Go的忠实粉丝把这种All in One的特性作为评价语言的标准,他们认为至少在这些方面,Go是可以完美的代替其他语言的。

那么,Go真的能优秀到完全替代另一个语言么?

其实未必,我始终认为银弹是不存在的[2],无论是在这次调查前,还是在这次调查后。

本文从Go语言被设计的初衷出发,深入互联网各种角落,调查Go所具有的那些特性是否足够优秀,同时和其他语言进行适当的比较,你可以选择性的阅读、接受或者反对我的内容,毕竟有交流才能传播知识。

我的最终目的是让更多的初学者看到Go没有轻易暴露出的缺点,同时也能看到Go真正优秀的地方

设计Go的初衷

Go语言的主要目标是将静态语言的安全性和高效性与动态语言的易开发性进行有机结合,达到完美平衡,从而使编程变得更加有乐趣,而不是在艰难抉择中痛苦前行[3]。

Google公司不可能无缘无故地设计一个新语言(一些特性相比于其他语言也没有新到哪里去),这一切肯定是有原因的。

设计Go语言是为了解决当时Google开发遇到的一些问题[4]:

  • C++编译慢、没有现代化(入门级友好的)的内存管理
  • 数以万计行的代码,难以维护
  • 部署的平台各式各样,交叉编译困难
  • ......

joke.png

找不到什么合适的语言,想着反正都是弄来自己用,Google选择造个轮子试试。

Go 语言起源 2007 年,并于 2009 年正式对外发布。它从 2009 年 9 月 21 日开始作为谷歌公司 20%兼职项目,即相关员工利用 20% 的空余时间来参与 Go 语言的研发工作。该项目的三位领导者均是著名的 IT 工程师:Robert Griesemer,参与开发 Java HotSpot 虚拟机;Rob Pike,Go 语言项目总负责人,贝尔实验室 Unix 团队成员,参与的项目包括 Plan 9,Inferno 操作系统和 Limbo 编程语言;Ken Thompson,贝尔实验室 Unix 团队成员,C 语言、Unix 和 Plan 9 的创始人之一,与 Rob Pike 共同开发了 UTF-8 字符集规范。自 2008 年 1 月起,Ken Thompson 就开始研发一款以 C 语言为目标结果的编译器来拓展 Go 语言的设计思想[3]。

go-designers.png

Go 语言设计者:Griesemer、Thompson 和 Pike [3]

当时Google的很多工程师是用的都是C/C++,所以语法的设计上接近于C,Go的设计师们想要解决其他语言使用中的缺点,但是仍保留他们的优点[5]:

  • 静态类型和运行时效率
  • 可读性和易用性
  • 高性能的网络和多进程
  • ...

emmm,这些听起来还是比较玄乎,毕竟设计归设计,实现归实现,我们回顾一下现在Go的几个主要特点,编译速度、执行速度、内存管理以及并发编程。

Go的编译为什么快

当然,设计Go语言也不是完全从零开始,最初Go的团队尝试设计实现一个Go语言的编译前端,由基于C的gcc编译器来编译成机器代码,这个面向gcc的前端编译器也就是目前的Go编译器之一的gccgo。

与其说Go的编译为什么快,不如先说说C++的编译为什么慢,C++也可以用gcc编译,编译速度的大部分差异很有可能来源于语言设计本身。

在讨论问题之前,其中需要先说明的一点是:这里比较的编译速度都是在静态编译下的

静态编译和动态编译的区别:

  • 静态编译:编译器在编译可执行文件时,要把使用到的链接库提取出来,链接打包进可执行文件中,编译结果只有一个可执行文件。
  • 动态编译:可执行文件需要附带独立的库文件,不打包库到可执行文件中,减少可执行文件体积,在执行的时候再调用库即可。

两种方式有各自的优点和缺点,前者不需要去管理不同版本库的兼容性问题,后者可以减少内存和存储的占用(因为可以让不同程序共享同一个库),两种方式孰优孰弱,要对应到具体的工程问题上,Go默认的编译方式是静态编译

回到我们要讨论的问题:C++的编译为什么慢?

C++编译慢的主要两个大头原因[6]

  • 头文件的include方式
  • 模板的编译

C++使用include方式引用头文件,会让需要编译的代码有乘数级的增加,例如当同一个头文件被同一个项目下的N个文件include时,编译器会将头文件引入到每一份代码中,所以同一个头文件会被编译N次(这在大多数时候都是不必要的);C++使用的模板是为了支持泛型编程,在编写对不同类型的泛型函数时,可以提供很大的便利,但是这对于编译器来说,会增加非常多不必要的编译负担。

当然C++对这两个问题有很多后续的优化方法,但是这对于很多开发者来说,他们不想在这上面有过多时间和精力开销。

大部分后来的编程语言在引入文件的方式上,使用了import module来代替include 头文件的方式,import解决了重复编译的问题,当然Go也是使用的import方式;在模板的编译问题上,由于Go在设计理念上遵循从简入手,所以没有将泛函编程纳入到设计框架中,所以天生的没有模版编译带来的时间开销(没有泛型支持也是很多人不满Go语言的理由)。

在Go 的1.5 版本中,Go团队使用Go语言来编写Go语言的编译器(也叫自举),相比于gccgo来说:

  • 提高了编译速度,但执行速度略有下降(性能细节优化还不如gcc)
  • 增加了可编译的平台类型(以往受限于gcc)

在此之外,Go语言语法中的关键字也是非常少的(Go1.11版本里只有25个)[7],这也可以减少编译器花费在语法解析上的时间开销。

keywords.png

所以在我看来,Go编译速度快,主要出于四个原因

  • 使用了import的引用管理方式;
  • 没有模板的编译负担;
  • 1.5版本后的自举编译器优化;
  • 更少的关键字。

所以为了加快编译速度、放弃C++而转入Go的同时,也要考虑一下是否要放弃泛型编程的优点。

注:泛型可能在Go 2版本获得支持。

Go的实际性能如何

Go的执行速度,可以参考一个语言性能测试数据网站 —— The Computer Language Benchmarks Game[8]。

这个网站在不同的算法上对每个语言进行测试,然后给出时间和内存上的开销数据比对。

比较的语言有C++、Java、Python。

首先是时间开销:

time-cost.png

注意时间开销的单位是s,并且Y轴为了方便进行不同跨度上的比较,所以选取的是对数轴(即非线性轴,为1-10-100-1000的比较跨度)。

然后是内存开销:

mem-cost.png

注意Y轴为了方便进行不同跨度上的比较,所以选取的是对数轴(即非线性轴,为1000-10000-100000-1000000的比较跨度)。

需要注意的是,语言本身的性能只决定了一个程序的最高理论性能,程序具体的性能还要取决于这个程序的实现方法,所以当各个语言的性能并没有太大的差异时,性能往往只取决于程序实现的方式。

通过两个图的数据可以分析:

  • Go虽然还无法达到C++那样的极致性能,但是在大部分情况下已经很接近了
  • Go和Java在算法的时间开销上难分伯仲,但在内存的开销上Java就要高得多了;
  • Go在上述的绝大部分情况下,至少时间和内存开销都比Python要优秀得多;

Go的并发编程

Go的并发之所以比较受欢迎,网络上的很多内容集中在几个方面:

  • 天生并发的设计
  • 轻量化的并发编程方式
  • 较高的并发性能
  • 轻量级线程Goroutines、并发通信Channels以及其他便捷的并发同步控制工具

由于Go在设计的时候就考虑到了并发的支持,或者说很多特性都是为了并发而设计,这和一些后期库支持并发和第三方库支持并发的语言不同。

所以Go的并发到底有多方便?在Go中使用并发,只需要在普通的函数执行前加上一个go关键字,就可以新建一个线程让函数在其中执行:

func main() {
    go loop() // 启动一个goroutine
    loop()
}

这样带来的好处不仅仅是让并发编程更方便了,在一些特定情况下,比如Go引用一些使用了并发的库时,这些库所使用的并发也是基于Go本身的并发设计,不会存在库使用另一套并发实现的情况,这样Go调度器在处理程序中的各种并发线程时,可以有更加统一化的管理方式。

不过Go的并发对于程序的实现要求还是比较高的,在使用一些通信Channel的场合,稍有疏忽就可能出现死锁的问题,比如:

fatal error: all goroutines are asleep - deadlock!

Go的并发量可以比大部分语言里普通的线程实现要高,这受益于轻量级的Goroutine,轻量化主要是它所占用的空间要小得多,例如64位环境下的JVM,它会默认固定为每个线程分配1MB的线程栈空间,而Goroutines大概只有4-8KB,之后再按需分配。足够轻量化的线程在相同的内存下也就可以有更高并发量(服务器CPU还没有饱和的情况下),同时也可以减少很多上下文切换的时间开销[9]。但是如果你的每个线程占用空间都非常大时(比如10MB,当然这是非常规需求的情况下),Go的轻量化优势就没有那么明显了。

Go在并发上的优点很明显,也是Go的功能目标,从语言设计上支持了并发,提供了统一便捷的工具,复杂的并发业务也需要在Go的一整套并发规范体系下进行编程,当然这肯定会牺牲部分实现自由度,但可以获得性能的提高和维护成本的下降。

PS:关于Go调度器的内容在这里并没有被提及,因为很难用简单的文字向读者说明该调度方式和其他调度方式的优劣,将在未来的某一篇中会细致地介绍Go调度器的内容。

Go的垃圾回收

垃圾回收(英语:Garbage Collection,缩写为GC),在计算机科学中是一种自动的存储器管理机制。当一个计算机上的动态存储器不再需要时,就应该予以释放,以让出存储器,这种存储器资源管理,称为垃圾回收。垃圾回收器可以让程序员减轻许多负担,也减少程序员犯错的机会[10]。

在使用Go或者其他支持GC的语言时,不用再像C++一样,手动地去释放不需要的变量占用的内容空间(free/delete)

的确,这很方便(对于懒人和容易忘记主动释放的人),但是也多了一些限制(暗箱操作的不透明性以及在GC处理上的性能开销)。GC也不是万能的,当遇到一些对性能要求较高的场景,还是需要记得进行一些主动释放或优化操作(比如说自定义内存池)。

PS:将在未来的某一篇中会细致地介绍Go垃圾回收的细节(如果你们也觉得有必要的话)。

什么时候可以选择Go?

Go有很多优点,编译快、性能好、天生并发以及垃圾回收,很多比较有特色的内容也还没有说到(比如gofmt)。

Go语言也有很多缺点,比如第三方库支持还不够多(相比于Python来说就少的太多了)、支持编译的平台还不够广、还有被称为噩梦的依赖版本管理(已经在改善了,但是还没有达到完全可靠的程度)。

所以到底Go适合做什么,不适合做什么?

分析了这么多后,这个问题其实很难回答,但我们可以选择先从不适合的领域把Go剔除掉,看看我们会剩下什么。

Go不适合做什么

  • 极致高性能优化的场景,你可能需要使用C/C++,甚至是汇编;
  • 简单流程的脚本工具、数值分析、深度学习,可能Python更适合(至少目前是);
  • 搭一个博客或网站,PHP何尝不是天下第一的语言呢;
  • 如果你想比较方便找到一份的后端工作,绝大部分公司的Java岗一直缺人(在实际生产过程中,目前Go仍没有比Java表现得好太多,至少没有好到让一个部门/公司将核心业务重新转向Go来进行重构);
  • ...

你可以找到类似上面那样的很多场景,你可能会发现Go并不能那么完美地替代掉谁。

Go适合做什么

最后,到了我们的终极问题,Go到底适合做什么?

读到这里你可能会觉得,好像是我把Go的特性吹了一遍,然后突然告诉你可能Go不适合你。

Go天生并发,面向并发,所以Go的定位一直很清楚,从最浅显的视角来看,至少Go作为一个有较高性能的并发后端来说,是具有非常大的诱惑力的。

尤其对于后端相关的程序员而言,在某些业务功能的初步实现上,简洁的语法、内置的并发、快速的编译,都可以让你更加高效快速地完成任务(前提是Go的内容足以完成你的任务),不用再去担忧编译优化和内存回收、不用担心过多的时间和内存开销、不用担心不同版本库之间的冲突(静态编译)以及不用担心交叉编译平台适配问题。

大部分情况下,编写一个服务,你只需要:实现、编译、部署、运行

高效快速,足够敏捷,这在企业的绝大部分项目的初期都是适用的,这也是大部分项目对开发初期的要求。当一个项目或者服务真的可以发展下去,需求的确触碰到Go的天花板时,再考虑使用更加好的语言或方法去优化也为时不晚。

简而言之,尽管Go的过于简洁带来了很多问题(有些人说的难听点叫过于简单),Go所具有的优点,可以让大部分人用编程语言这种工具,来解决对他们而言更加重要的问题。

Go语言不是银弹,但它的确能有效地解决这些问题。

参考文章

扩展阅读

在调查Go的过程中,发现了一些比较有意思、或者比较实用的文章,一并附在这里。

  • 我为什么选择使用 Go 语言?,该文写于2016年,在我的文章基本构思完成的时候,偶然看到了这篇文章,作者有很多早期Go版本的开发经验,里面有更多的细节都是出自于工程师的经验之谈,我发现其中的部分想法和我不谋而合,你可以把这篇文章当作本文的后续扩展阅读,不过要注意文章的时效,可能提及到的一些Go的缺点现在已经被改进了。
  • C/C++编译器的工作过程,主要是供不熟悉C系的朋友了解一下编译器的工作过程。
  • The Computer Language Benchmarks Game,一个对各个语言进行性能测试的网站,里面的算法具有一定的代表性,但是不能代表所有工程可能遇到的情况,仅供参考。
  • 为什么 Go 语言在某些方面的性能还不如 Java?,这是知乎上一个2017年开始有的问题,你可以看到很多人对于这个问题的分析,从多个角度来理解语言之间的性能差异。
  • go-wiki WhyGo,Go的Github仓库上维护的Wiki中,有一篇关于WhyGo的文章整理,不过大部分是英文,里面主要是很多关于“为什么我要选择Go”的软硬稿。
  • 为什么要使用Go语言,Go语言的优势在哪里,这个知乎的提问更早,是来自2013年的Yvonne YU用户,在Go的早期其实是具有很大的争议的,你可以看到大家在各个问题上的博弈。
  • 哪些公司在使用Go,Go的Github仓库上维护的Wiki中,有一篇关于全球都有哪些公司在使用Go,不过提供的信息大部分只有一个公司名,比如国内有阿里巴巴(而人家大部分都招Java),可以看看但参考性不大。
  • Go 语言的优点,缺点和令人厌恶的设计,这是Go语言中文网上一篇2018年的文章,如果你对语言本身的一些特性的设计感兴趣,你可以选择看看,作者从很多语法层面上介绍了Go的优点和缺点。
  • Ruby China - 瞎扯淡 真的没必要浪费心思在 Go 语言上,这是我无意中找到的一篇有名的帖子,这个问题始于2013年,在Ruby China上,其中也是大佬们(可能)从各个角度来辩论Go是否值得学习,可以当作武侠小说观看。
  • The way to Go - 3.8 Go性能说明,《The way to Go》这本书上为数不多关于Go性能问题的说明。
  • C++开发转向go开发是否是一个好的发展方向?,2014年知乎上关于C++和Go的一个讨论,其实我觉得“如果选择一个并不意味着就要放弃另一个”,程序员不是研究语言的,也不应该是只靠某一门语言吃饭。
  • 我为什么放弃Go语言 Liigo,嗯,2014年,仍旧是Go争议很大的时候,CSDN上一篇阅读数很高的文章,作者从自己的角度对Go进行批判(Go早期的确是有不少问题),你可以看到早期Go的很多问题,也可以斟酌这些问题对你是否重要以及到底在2020年的Go中有没有被解决。
  • Golang 本身是用什么语言写的?,一个关于编译的有趣的问题,可以适当了解。
  • 搞懂Go垃圾回收,一篇还算比较新的分析Go垃圾回收问题的文章。
  • 有趣的编程语言:Go 语言的启动时间是 C 语言的 300 多倍,C# 的关键字最多,这篇InfoQ文章其实算是一个典型的标题党,主要使用的是一个Github上关于各个语言HelloWorld程序启动时间的测试数据(https://github.com/bdrung/sta...,使用gccgo编译的Go程序的启动时间非常地长,的确是C的300多倍,但使用GC编译的Go程序启动时间只是C的2倍。
  • Go 语言的历史回顾,我一直在寻找一个整理Go的版本变动细节的文章,在Go的官方文档和各种书籍上寻找无果时,在InfoQ上找到了一篇还算跟踪地比较新的(Go 1.0 - Go 1.13)文章,对于初学者而言,知道语言的变化也是很重要的(比如方便的知道哪些问题解决了,哪些还没有被解决),可能之后会拓展性的写一篇关于这个的文章。
查看原文

赞 20 收藏 9 评论 3

Yumiku 关注了标签 · 2020-03-08

docker

an open source project to pack, ship and run any application as a lightweight container ! By Lock !

关注 40704

Yumiku 关注了标签 · 2020-03-08

flutter

clipboard.png

Flutter 是 Google 用以帮助开发者在 iOS 和 Android 两个平台开发高质量原生 UI 的移动 SDK。

Flutter is Google’s mobile app SDK for crafting high-quality native interfaces on iOS and Android in record time. Flutter works with existing code, is used by developers and organizations around the world, and is free and open source.

Flutter 官网:https://flutter.dev/
Flutter 中文资源:https://flutter-io.cn/
Flutter Github:https://github.com/flutter/fl...

关注 992

Yumiku 关注了标签 · 2020-03-08

golang

Go语言是谷歌2009发布的第二款开源编程语言。Go语言专门针对多处理器系统应用程序的编程进行了优化,使用Go编译的程序可以媲美C或C++代码的速度,而且更加安全、支持并行进程。
Go语言是谷歌推出的一种全新的编程语言,可以在不损失应用程序性能的情况下降低代码的复杂性。谷歌首席软件工程师罗布派克(Rob Pike)说:我们之所以开发Go,是因为过去10多年间软件开发的难度令人沮丧。Go是谷歌2009发布的第二款编程语言。

七牛云存储CEO许式伟出版《Go语言编程
go语言翻译项目 http://code.google.com/p/gola...
《go编程导读》 http://code.google.com/p/ac-m...
golang的官方文档 http://golang.org/doc/docs.html
golang windows上安装 http://code.google.com/p/gomi...

关注 26192

Yumiku 关注了标签 · 2020-03-08

关注 511

Yumiku 发布了文章 · 2019-04-13

《廖雪峰JavaScript-快速入门》笔记

文章内容来源:廖雪峰JavaScript-快速入门

赋值

var x = 1;

注释

// comment
/* comment */

数据类型

Number

JavaScript不区分整数和浮点数,统一用Number表示。
123;
0.456;
1.2345e3;
NaN;
Infinity;

字符串

字符串是以单引号'或双引号"括起来的任意文本。
'Hello ';
"World!";

布尔

一个布尔值只有true、false两种值。

数组

JavaScript的数组可以包括任意数据类型。
[1, 2, 3.14, 'Hello', null, true];

对象

JavaScript的对象是一组由键-值组成的无序集合,对象的键都是字符串类型,值可以是任意数据类型。
var mi = {
  name: 'YuMi',
  age: 22
}
要获取一个对象的属性,我们用对象变量.属性名的方式:
mi.name;
mi.age;

变量名

变量名是大小写英文、数字、$_的组合,且不能用数字开头。

条件判断

JavaScript使用if () { ... } else { ... }来进行条件判断。

循环

for

for(var i = 0; i < arr.length; i++) {
  x = arr[i];
  console.log(x);
}

for...in

for循环的一个变体是for ... in循环,它可以把一个对象的所有属性依次循环出来:
var mi = {
  name: 'YuMi',
  age: 22
};
for (var key in mi) {
  console.log(key); // 'name', 'age', ...
}
要过滤掉对象继承的属性,用hasOwnProperty()来实现。
var mi = {
    name: 'YuMi',
    age: 22
};
for (var key in mi) {
  if(mi.hasOwnProperty(key)) {
    console.log(key); // 'name', 'age'
  }
}
for ... in循环可以直接循环出Array的索引。
var a = ['A', 'B', 'C'];
for (var i in a) {
  // 得到的 i 是 String 而不是 Number 。
  console.log(i); // '0', '1', '2'
  console.log(a[i]); // 'A', 'B', 'C'
}

while/do...while

略。

Map/Set

Map

最新的ES6规范引入了新的数据类型MapMap是一组键值对的结构,具有极快的查找速度。

初始化Map需要一个二维数组:

var m = new Map([['Michael', 95], ['Bob', 75], ['Tracy', 85]]);
m.get('Michael'); // 95
或者直接初始化一个空Map
var m = new Map(); // 空Map
m.set('Adam', 67); // 添加新的key-value
m.set('Bob', 59);
m.has('Adam'); // 是否存在key 'Adam': true
m.get('Adam'); // 67
m.delete('Adam'); // 删除key 'Adam'
m.get('Adam'); // undefined
多次对一个key放入value,后面的值会把前面的值冲掉。

Set

SetMap类似,也是一组key的集合,但不存储value

要创建一个Set,需要提供一个Array作为输入,或者直接创建一个空Set

var s1 = new Set(); // 空Set
var s2 = new Set([1, 2, 3]); // 含1, 2, 3
s2.add(4);
s2.delete(3);

iterable

为了统一集合类型,ES6标准引入了新的iterable类型,ArrayMapSet都属于iterable类型。

具有iterable类型的集合可以通过新的for ... of循环来遍历。

for ... in循环由于历史遗留问题,它遍历的实际上是对象的属性名称。一个Array数组实际上也是一个对象,它的每个元素的索引被视为一个属性。

var a = ['A', 'B', 'C'];
a.name = 'Hello';
for (var x in a) {
    console.log(x); // '0', '1', '2', 'name'
}
for ... of循环则完全修复了这些问题,它只循环集合本身的元素:
var a = ['A', 'B', 'C'];
a.name = 'Hello';
for (var x of a) {
    console.log(x); // 'A', 'B', 'C'
}
更好的方式是直接使用iterable内置的forEach方法,它接收一个函数,每次迭代就自动回调该函数:
var a = ['A', 'B', 'C'];
a.forEach(function (element, index, array) {
    // element: 指向当前元素的值
    // index: 指向当前索引
    // array: 指向Array对象本身
    console.log(element + ', index = ' + index);
});
SetArray类似,但Set没有索引,因此回调函数的前两个参数都是元素本身:
var s = new Set(['A', 'B', 'C']);
s.forEach(function (element, sameElement, set) {
    console.log(element);
});
Map的回调函数参数依次为valuekeymap本身:
var m = new Map([[1, 'x'], [2, 'y'], [3, 'z']]);
m.forEach(function (value, key, map) {
    console.log(value);
});

个人静态博客:

查看原文

赞 0 收藏 0 评论 0

Yumiku 发布了文章 · 2019-02-13

前端菜鸟笔记 Day-5 CSS 高级

文章大纲来源:【Day 5】CSS 高级

  • CSS 选择器
  • CSS 拓展
  • CSS 单位
  • CSS 参考手册

CSS 选择器

内容引用:CSS 选择器

元素选择器

html { ... }

选择器分组

h2, p { ... }

类选择器

.important { ... }
p.warning  { ... }
.important.warning { ... }
/* 选择同时拥有这两个类名的元素 */

ID选择器

#intro { ... }

属性选择器

a[href] { ... }
a[href][title] { ... }
a[href="..."] { ... }
p[class="important warning"] { ... }
/* 完全匹配的属性内容 */
p[class~="important"] { ... }
/* 部分匹配的属性内容 */

后代选择器

h1 em { ... }

子元素选择器

h1 > strong { ... }

相邻兄弟选择器

h1 + p { ... }

伪类

CSS 伪类用于向某些选择器添加特殊的效果。

语法是selector : pseudo-class {property: value}

a:link { color: #FF0000 }        /* 未访问的链接 */
a:visited { color: #00FF00 }    /* 已访问的链接 */
a:hover { color: #FF00FF }    /* 鼠标移动到链接上 */
a:active { color: #0000FF }    /* 选定的链接 */
p:first-child { font-weight: bold; }

CSS 拓展

内容引用:CSS 高级

水平对齐

  • 使用margin:auto水平对齐
margin-left:auto;
margin-right:auto;
  • 使用position左或右对齐
position:absolute;
right:0px;
  • 使用float左或右对齐
float:right;

尺寸

  • height:元素高度
  • width:元素宽度
  • line-height:行高
  • max-height:最大高度
  • max-width:最大宽度
  • min-height:最小高度
  • min-width:最小宽度

CSS 单位

内容引用:CSS 单位

相对长度

指定了一个长度相对于另一个长度的属性,对于不同的设备相对长度更适用。
  • em:相对于应用在当前元素的字体尺寸,一般浏览器字体大小默认为16px,则2em == 32px
  • ex:依赖于英文子母小 x 的高度
  • ch:数字 0 的宽度
  • rem:根元素(html)的 font-size
  • vw:viewpoint width,视窗宽度,1vw=视窗宽度的1%
  • vh:viewpoint height,视窗高度,1vh=视窗高度的1%

绝对长度

绝对长度单位是一个固定的值,它反应一个真实的物理尺寸。

绝对长度单位视输出介质而定,不依赖于环境(显示器、分辨率、操作系统等)。

  • cm:厘米
  • mm:毫米
  • in:英寸(1in = 96px = 2.54cm)
  • px:像素 (1px = 1/96th of 1in)
  • pt:point,大约1/72英寸; (1pt = 1/72in)
  • pc:pica,大约6pt,1/6英寸; (1pc = 12 pt)

CSS 参考手册

使用时如果有疑问可以随时查看【CSS 参考手册】


个人静态博客:

查看原文

赞 0 收藏 0 评论 0

Yumiku 发布了文章 · 2019-02-03

前端菜鸟笔记 Day-4 CSS布局

文章大纲来源:【Day 3】HTML复习 + CSS基础

  • CSS框模型
  • 宽度和高度
  • 内边距
  • 外边距
  • CSS定位
  • 浮动

CSS框模型

内容引用:CSS 框模型概述

CSS 框模型 (Box Model) 规定了元素框处理元素内容、内边距、边框 和 外边距 的方式。

图片描述

元素的背景应用于**由内容和内边距(padding)、边框(border)组成的区域。

边框以外是外边距(margin),外边距默认是透明的,因此不会遮挡其后的任何元素。

内边距、边框和外边距都是可选的,默认值是零;内边距、边框和外边距可以应用于一个元素的所有边,也可以应用于单独的边。

宽度和高度

定义元素的宽高属性。

  • 宽度 width
  • 高度 height

可以用px进行数字定义,如1px;也可以用百分比100%等表示,百分比表示占父元素的百分之多少。

注:行内元素不能定义宽高,块元素和行内块元素可以。

内边距

内容引用:CSS 内边距

元素的内边距在边框和内容区之间。

padding 属性定义元素的内边距,接受长度值或百分比值,但不允许使用负值。

h1 { padding: 10px; }

还可以按照上、右、下、左的顺序分别设置各边的内边距,各边可以使用不同的单位或者百分比值:

h1 { padding: 10px 0.25em 2ex 20%; }

单边内边距属性

也可以使用下面四个单独的属性分别设置:

  • padding-top
  • padding-right
  • padding-bottom
  • padding-left
h1 {
  padding-top: 10px;
  padding-right: 0.25em;
  padding-bottom: 2ex;
  padding-left: 20%;
}

内边距的百分比

之前在宽高设置部分使用百分比,可以相对父元素的宽高设置。

内边距的百分数值是相对于父元素的宽度计算的。

/* 段落的内边距设置为父元素 width 的 10% */
p { padding: 10%; }

注意上面解释定义的部分padding只参考了父元素的width,也就是上下内边距也是参照的width,而不是参照常理上父元素的heightpadding-top/padding-bottom也是一样参照的width

外边距

内容引用:CSS 外边距

围绕在元素边框的透明区域是外边距。

设置外边距就是使用 margin 属性,这个属性接受任何长度单位(像素、英寸、毫米或 em)、百分数值甚至负值

margin 可以设置为 auto

基本上外边距和内边距padding书写方式类似,甚至在百分数参考父元素width这一点上也是一样的。

单边外边距属性

单边内边距属性类似:

  • margin-top
  • margin-right
  • margin-bottom
  • margin-left

不再更多的说明。

值复制

有时会输入一些重复的值:

p { margin: 0.5em 1em 0.5em 1em; }

通过值复制,可以不必重复的声明属性:

/* 上面的规则与下面的规则是等价的 */
p { margin: 0.5em 1em; }

CSS 定义了一些规则,允许为外边距指定少于 4 个值:

  • 缺少 左,则使用 右 的值
  • 缺少 下,则使用 上 的值
  • 缺少 右,则使用 上 的值
h1 { margin: 0.25em 1em 0.5em; }
/* 等价于 0.25em 1em 0.5em 1em */
h2 { margin: 0.5em 1em; }
/* 等价于 0.5em 1em 0.5em 1em */
p { margin: 1px; }
/* 等价于 1px 1px 1px 1px */

CSS定位

内容引用:CSS 定位 (Positioning)

CSS 定位 (Positioning) 属性允许你对元素进行定位。

定位的基本思想很简单,它允许你定义元素框相对于其正常位置应该出现的位置,或者相对于父元素、另一个元素甚至浏览器窗口本身的位置。

一切皆为框

divh1p 元素常常被称为块级元素。这意味着这些元素显示为一块内容,即“块框”。

与之相反,spanstrong 等元素称为行内元素,这是因为它们的内容显示在行中,即“行内框”。

可以使用 display 属性改变生成的框的类型。

如果一个框的属性设置为display:none,该框及其所有内容就不再显示,不占用文档中的空间。

但是一种情况下,即使没有显式定义(包括环绕标签),也会创建块级元素,这种情况发生在把一些文本添加到一个块级元素(比如 div)的开头。即使没有把这些文本定义为段落,它也会被当作段落对待:

<div>
some text
<p>Some more text.</p>
</div>

在这种情况下,这个框称为无名块框,因为它不与专门定义的元素相关联。

定位机制

CSS 有三种基本的定位机制:普通流、浮动和绝对定位。

所有框默认都在普通流中定位。

块级框从上到下一个接一个地排列,框之间的垂直距离是由框的垂直外边距计算出来。

行内框在一行中水平布置。可以使用水平内边距、边框和外边距调整它们的间距。但是,垂直内边距、边框和外边距不影响行内框的高度

由一行形成的水平框称为行框(Line Box),行框的高度总是足以容纳它包含的所有行内框。不过,设置行高可以增加这个框的高度。

position 属性

通过使用 position 属性,我们可以选择 4 种不同类型的定位。
  • static :元素框正常生成。
  • relative元素框偏移某个距离。元素仍保持其未定位前的形状,它原本所占的空间仍保留。
  • absolute :元素框从文档流完全删除,并相对于其包含块定位。
  • fixed :类似于将 position 设置为 absolute,不过其包含块是视窗本身。

相对定位

内容引用:CSS 相对定位

设置为相对定位的元素框会偏移某个距离。元素仍然保持其未定位前的形状,它原本所占的空间仍保留。

简单来说就是,原来所占位置还是占那个位置,但是元素将会进行偏移显示

#box_relative {
  position: relative;
  /* 框将在原位置顶部下面20像素的地方 */
  top: 20px;
  /* 框将在原位置左部右边30像素的地方 */
  left: 30px;
}

绝对定位

内容引用:CSS 绝对定位

设置为绝对定位的元素框从文档流完全删除,并相对于其包含块定位。

绝对定位使元素的位置与文档流无关,因此不占据空间,这一点与相对定位不同。

简单来说就是,元素不再占用任何文档流的空间,只剩下相对于包含块的定位显示

#box_relative {
  position: absolute;
  /* 框将在包含块顶部下面20像素的地方 */
  top: 20px;
  /* 框将在包含块左部右边30像素的地方 */
  left: 30px;
}

注意以上说明的包含块的概念是:

绝对定位的元素的位置相对于最近的已定位祖先元素,如果元素没有已定位的祖先元素,那么它的位置相对于最初的包含块(一般情况下是HTML元素)。

上述概念中,已定位指的就是position属性设置了relativeabsolutefixed之一的元素;最近的已定位指的是元素父子链往从本元素向上寻找,其中最近的已定位祖先元素。

提示:因为绝对定位的框与文档流无关,所以它们可以覆盖页面上的其它元素。可以通过设置 z-index 属性来控制这些框的堆放次序。

浮动

内容引用:CSS 浮动

浮动的框可以向左或向右移动,直到它的外边缘碰到包含框或另一个浮动框的边框为止。

由于浮动框不在文档的普通流中,所以文档的普通流中的块框表现得就像浮动框不存在一样。

如果包含框太窄,无法容纳水平排列的三个浮动元素,那么其它浮动块向下移动,直到有足够的空间。如果浮动元素的高度不同,那么当它们向下移动时可能被其它浮动元素“卡住”。

float属性

使用浮动的方法:

img {
  /* 把图像向右浮动 */
  float: right;
}

float可能的值:

  • none :默认值,元素不浮动,并会显示在其在文本中出现的位置。
  • left :元素向左浮动。
  • right :元素向右浮动。
  • inherit :从父元素继承 float 属性的值。

行框和清理

浮动框旁边的行框会被缩短,使行框围绕浮动框,所以创建浮动框可以使文本围绕图像。

注释:这里说的行框就是之前说的无名块框。

如果想要阻止行框围绕浮动框,需要对该框(?)应用clear属性,属性值可以是leftrightbothnone它表示框的哪些边不应该挨着浮动框

在这里将不会进一步详细的说明浮动和清理的深入用法和机制说明(主要是自己暂时不太喜欢用,到时候涉及到的时候再开专题说明吧,咕咕咕)。


个人静态博客:

查看原文

赞 3 收藏 3 评论 0

Yumiku 发布了文章 · 2019-02-02

前端菜鸟笔记 Day-3 CSS基础

文章大纲来源:【Day 3】HTML复习 + CSS基础

  • 初识CSS
  • 入门CSS

初识CSS

层叠样式表(Cascading Style Sheets),即前端常说的CSS。

内容引用:CSS 简介

样式解决了什么问题?

HTML标签原本被设计为用于定义文档内容

通过使用标签来表达语义信息

那个时候的文档布局由浏览器实现,没有使用什么格式的标签。

但是当时主要的浏览器(Netscape 和 Internet Explorer)不断地将新的HTML标签和属性(比如字体颜色等)加入到HTML规范中,文档内容要独立于文档表示层越来越困难(各家都有各家的HTML书写属性规范,并且样式一般不统一)。

为了解决这个问题,万维网联盟(W3C)在HTML 4.0 之外创造出样式(Style)。

样式表如何提高工作效率?

样式表(Style Sheets)定义如何显示 HTML 元素

样式通常保存在外部的.css文件中,并且可以被多个.html文件所引用,所以外部的样式表可以一处修改,多处协同影响

CSS的使用方法

一共有三种:

标签内属性定义

<!-- learn.html -->
<body style="background: red;">
   ...
</body>

内部定义

<!-- learn.html -->
<style>
body {
  background: red;
}
</style>

外部定义

<!-- learn.html -->
<head>
  <link rel="stylesheet" type="text/css" href="style.css" />
</head>
/* style.css */
body {
  background: red;
}

外部定义可以<link>多个样式表,书写多个<link>标签引用即可。

那“层叠”指的是什么?

样式表允许多种方式规定样式信息。
  1. HTML元素属性中
  2. HTML的头元素中
  3. 外部的CSS文件中
  4. 同一个文档引用多个外部样式表

那么就容易遇到一个问题:“当同一个 HTML 元素被不止一个样式定义时,会使用哪个样式呢?”

这就是一种层叠了,即多重样式将层叠为一个

在这个层叠过程中,就需要一种层叠次序,来选择最后到底选择哪一个样式:

  • 最高 —— 内联样式,即 HTML 元素内部
  • 高 —— 内部样式表,即<head>标签内部
  • 中 —— 外部样式表
  • 最低 —— 浏览器缺省样式

拥有高次序的样式会覆盖低次序的样式定义。

入门CSS

基础语法

CSS规则 由两个主要部分构成:选择器,以及声明
selector {
  declaration1;
  declaration2;
  ...
  declarationN;
}
  • 选择器(selector)用于选择需要改变的 HTML 元素
  • 声明(declaration)定义需要改变的属性和值,每条声明由一个属性和一个值组成(property: value)。

例如:

h1 {
  color: red;
  font-size: 14px;
}

<h1>元素内的文字颜色定义为红色,同时字体大小设置为14像素。

内容引用:CSS 基础语法

语法补充

除了基础语法,还有一些需要补充的内容。

内容引用:CSS 高级语法

选择器分组

被分组的选择器可以分享相同的声明。
h1,h2,h3,h4,h5,h6 {
  color: green;
}

使用逗号将需要分组到一组的选择器连接在一起即可。

样式继承

根据CSS,子元素从父元素继承属性。
body {
  font-family: Verdana, sans-serif;
}

上述规则说明,<body>元素将使用Verdana字体(如果访问者系统中有的话)。

通过CSS继承,子元素将继承最高级元素(上面的例子是<body>)所拥有的属性。这里的子元素指的就是在<body>标签内声明的那些所有标签(其实并不一定是所有,具体取决于浏览器支持度)。

但是在使用CSS继承规则时,如果不希望一个特定子元素继承该CSS,则再说明一条特殊规则来覆盖即可(这部分涉及到选择器优先级问题,将在后面说明)。

body {
  font-family: Verdana, sans-serif;
}
p {
  font-family: Times, "Times New Roman", serif;
}

CSS 派生选择器

依据元素的位置关系来定义样式。

CSS1称其为上下文选择器(contextual selectors),CSS2称其为派生选择器。

例子:

li strong {
  font-style: italic;
  font-weight: normal;
}
<p>
  <strong>我是粗体字,不是斜体,因为这个规则对我不起作用</strong>
</p>
<ol>
  <li>
  <strong>我是斜体字。这是因为 strong 元素位于 li 元素内。</strong>
  </li>
  <li>我是正常的字体。</li>
</ol>

只有 li 元素中的 strong 元素的样式为斜体字,这样无需为需要修饰的 strong 元素单独定义 class或者id,代码更加简洁。

派生选择器还有更加深入的内容:

  • CSS 后代选择器
  • CSS 子元素选择器
  • CSS 相邻兄弟选择器

内容引用:CSS 派生选择器

CSS id选择器

为标有 特定id 的 HTML 元素指定特定的样式。

id选择器以"#"来定义。

#red { color: red; }
#green { color: green; }
<p id="red">这个段落是红色。</p>
<p id="green">这个段落是绿色。</p>

之所以叫特定的,就是因为 id属性 只能在每个HTML文档中出现一次

在现在布局中,id选择器常常用于建立派生选择器

#sidebar p {
  ...
}

样式只会应用于出现在id是sidebar的元素内的段落。

内容引用:CSS id 选择器

CSS 类选择器

类选择器的功能可以简单看成是:能给多个元素相同id的id选择器,只不过这里不再是用id了,而是用class。

在CSS中,类选择器以一个点号显示。
.center {
  text-align: center;
}

所有拥有center类的HTML元素都会应用这个样式。

<h1 class="center">
This heading will be center-aligned
</h1>

<p class="center">
This paragraph will also be center-aligned.
</p>

注意:类名的第一个字符不能是数字,否则无法在 Mozilla 或 Firefox 中起作用。

和 id 一样,class 也可被用作派生选择器。
.sidebar p {
  ...
}

内容引用:CSS 类选择器

CSS 属性选择器

对带有指定属性的 HTML 元素设置样式,不仅限于 class 和 id 属性。

注意:只有在规定了 !DOCTYPE 时,IE7 和 IE8 才支持属性选择器。在 IE6 及更低的版本中,不支持属性选择。

属性选择器:

/* 带有 title 属性的所有元素 */
[title] {
  color: red;
}

属性和值选择器:

/* title="W3School" 的所有元素 */
[title=W3School] {
  border: 5px solid blue;
}

如果一个属性有多个值,想要选中这样的元素:

/* 适用于由空格分隔的属性值 */
[title~=hello] {
  color: red;
}
/* 适用于由连字符分隔的属性值 */
[lang|=en] {
  color: red;
}

这种方法的一个应用是设置表单的样式:

input[type="text"] {
  ...
}
input[type="button"] {
  ...
}
...

内容引用:CSS 属性选择器


个人静态博客:

查看原文

赞 0 收藏 0 评论 0

Yumiku 发布了文章 · 2019-01-31

前端菜鸟笔记 Day-2 Form表单

文章大纲来源:【Day 2】Form表单

  • HTML表单
  • 表单元素
  • 表单属性
  • HTML5追加的表单元素

HTML表单

HTML 表单用于搜集不同类型的用户输入。

<form>

<form>标签定义 HTML 表单:

<form>
  ...
  form elements
  ...
</form>

表单元素

HTML 表单包含表单元素。

表单元素指的是不同类型的:

  • input元素
  • 复选框
  • 单选按钮
  • 提交按钮
  • 等等

内容引用:HTML表单元素

<input>

最重要的表单元素。

<input> 元素根据不同的 type 属性可以变化为多种形态。

<form>
  ...
  <input type="..." ...>
  ...
</form>

text

单行输入框。

<input type="text" name="usr">

password

字符掩码处理的单行输入框。

<input type="password" name="psw">

submit

一个提交按钮。

<input type="submit">

至于用哪个程序来处理提交的表单数据,在<form>标签中的action属性中定义。

<form action="action.php">
  ...
  <input type="submit" value="Submit">
</form>

其中的value属性定义按钮上显示的文字,缺省会显示默认文本(中文环境下为“提交”)。

radio

定义单选按钮。

<form>
  <input type="radio" name="sex" value="male" checked>Male
  <br>
  <input type="radio" name="sex" value="female">Female
</form>

其中的name属性非常重要,多个radio类型的<input>只有在name属性相同时才具有单选限制。

checkbox

定义多选框,允许选0个或多个。

<form>
<input type="checkbox" name="vehicle" value="Bike">I have a bike
<br>
<input type="checkbox" name="vehicle" value="Car">I have a car
</form>

name属性作用类似radio

button

定义按钮

<input type="button"><button>标签的异同会在之后单独的专题说明。

<input type="button" onclick="alert('Hello World!')" value="Click">
  • onclick定义触发的方法
  • value定义按钮显示文字

file

用于选取文件和上传文件。

<input type="file" name="pic" accept="image/gif" />

涉及到的时候会在之后单独的专题说明。

reset

定义重置按钮,触发后会清楚表单的所有数据。

<input type="reset" value="Reset">

<select>

定义下拉列表。

<select name="cars">
  <option value="volvo">沃尔沃</option>
  <option value="mazda">马自达</option>
  <option value="hevrolet">雪佛兰</option>
  <option value="audi">奥迪</option>
</select>

<option>定义待选择的选项,列表通常会默认选择第一个选项,可以使用slected属性来定义预定义选项。

<option value="mazda" selected>马自达</option>

<textarea>

定义多行输入字段。

<textarea name="message" rows="10" cols="30">
The cat was playing in the garden.
</textarea>

<button>

定义可点击的按钮

<button type="button" onclick="alert('Hello World!')">Click</button>

表单属性

  • value(通用)
  • name(通用)
  • readonly(通用)
  • disable(通用)
  • type(重要)
  • checked(radio、checkbox,重要)
  • size
  • maxLength

内容引用:HTML Input 属性

value

value属性规定输入字段的初始值,和按钮的显示文字

<input type="text" name="firstname" value="John">

readonly

readonly属性规定输入字段只读(不可修改)

<input type="text" name="firstname" value="John" readonly>

属性不用赋值,等同于readonly="readonly"

disabled

disabled属性规定输入字段是禁用的(不可用和不可点击)

并且也不会被提交(与readonly不同)。

<input type="text" name="firstname" value="John" disabled>

属性不用赋值,等同于disabled="disabled"

size

size属性规定输入字段的尺寸(以字符计)

这里的尺寸,具体指的是类似输入框宽度的属性。

<input type="text" name="firstname" value="John" size="40">

maxlength

maxlength属性规定输入字段允许的最大长度

<input type="text" name="firstname" maxlength="10">

超过长度的字符不会被接受(也就是输入不进去),但是用户超过时,input元素本身不会有任何提示。

HTML5追加的表单元素

了解内容,主要是一些新增的Input类型:

  • email
  • url
  • number
  • range
  • Date pickers
  • search
  • color

目前开发的经验来看,这类组件如果对UI统一需求不高的话,可以尝试使用一点,不过一般情况下都会造轮子或者用现成较为成熟的轮子来代替使用这些。

内容连接:HTML5 Input 类型


个人静态博客:

查看原文

赞 0 收藏 0 评论 0

Yumiku 发布了文章 · 2019-01-30

前端菜鸟笔记 Day-1 HTML&HTML 5

文章大纲来源:【Day 1】HTML & HTML 5

  • 标记语言
  • XHTML/HTML/HTML 5异同
  • 了解doctype
  • HTML
  • HTML 5

标记语言

标记语言(ML)即 Markup Language,可以准确定义数据信息本身以及和数据相关的信息。

其中标记(markup)这个词,来源于传统出版业的“标记”手稿,也就是在原稿边缘加注一些符号来指示打印上的要求(字体段落的要求)。

在这个例子中,原稿本身就是数据信息,加注的指示就是和数据相关的信息

HTML/XHTML/HTML 5异同

HTML 简述

HTML 是超文本标记语言 (HyperType Markup Language) 的简称,HTML是用来描述网页的一种语言。

XHTML 简述

XHTML 是可扩展超文本标签语言 (EXtensible HyperText Markup Language)的简称, XHTML 的目标是用规范化语法结构来取代 HTML ,以 XML 为根本重构了 HTML 4.01 。

HTML 5 简述

HTML 5 的设计目的是为了在移动设备上支持多媒体。

新的语法特性被引进以支持这一点,如videoaudiocanvas标记 (tag) 。

HTML 5 将会取代1999年制定的 HTML 4.01、XHTML 1.0 标准。

三者异同点

  • HTML 5 和 XHTML 是老版 HTML 的替代
  • HTML 5 主要用来在移动设备上支持多媒体
  • XHTML 为了用来严格规范语法结构
  • HTML/XHTML/HTML 5 仅仅是版本不同
  • 目前 HTML 5 是主流

原文引用:

拓展阅读:

doctype

<!DOCTYPE> 声明帮助浏览器正确地显示网页。

<!DOCTYPE> 不是 HTML 标签。它为浏览器提供一项信息(声明),即 HTML 是用什么版本编写的。

HTML 5 声明:

<!DOCTYPE html>

HTML 4.01 声明:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
"http://www.w3.org/TR/html4/loose.dtd">

XHTML 1.0 声明:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

内容引用:HTML <!DOCTYPE>

HTML

基本格式

标准的HTML 5文档的格式:

<!DOCTYPE html>
<!--文档类型声明,不区分大小写,主要是告诉浏览器当前的文档类型-->
<html>
<!-- 表示html文档开始 -->
    <head>
    <!-- 包含文档元数据开始  -->
        <meta charset="UTF-8">
        <!-- 声明字符编码 -->
        <title>Title Tag</title>
        <!-- 设置文档标题 -->
    </head>
    <!-- 包含文档元数据接受 -->
    <body>
    <!-- 表示html内容部分开始,也就是可见部分 -->
    </body>
    <!-- 表示html内容部分结束 -->
</html>
<!-- 表示html文档结束 -->

内容引用:HTML 5的基本格式

块级(block)元素

块级元素最常使用的是div,其他的还有hX、p、nav、aside、header、footer、section、article、ul-li、address等等,也可以对任意元素进行display:block属性设置。

块级元素特征:

  • 设置宽高属性有效
  • marginpadding上下左右(水平垂直)均有效
  • 内容会自动进行换行
  • 多个块状元素标签写在一起,默认排序从上到下

行内(inline)元素

行内元素最常使用的是span,其他的还有a、code、i、img、input、textarea等等,也可以对任意元素进行display:inline属性设置。

行内元素特征:

  • 设置宽高属性无效
  • margin设置仅左右(水平)方向有效,上下(垂直)无效
  • padding设置上下左右都有效
  • 内容不会自动进行换行

行内块(inline-block)元素

行内块元素综合了两者的特征,各有取舍,可以对任意元素进行display:inline-block属性设置。

行内块元素特征:

  • 内容不会自动进行换行
  • 能够识别宽高
  • 多个行内块元素默认排列方式从左到右

HTML tag

也不用每个都详细说一下,后面有时间的话开一个专题挑几个常用的出来详细说一下。

HTML 参考手册

语义化

语义化的含义就是用正确的标签做正确的事情。

HTML语义化就是让页面的内容结构化,便于浏览器、搜索引擎(机器)理解解析,利于SEO。

内容引用:前端工程师手册-HTML语义化

script/style/link

<script>标签用于在 HTML 页面中插入一段 JavaScript 。

<script type="text/javascript">
document.write("Hello World!")
</script>

script元素既可以包含脚本语句(如上),也可以通过src属性指向外部脚本文件:

<script data-original=".../filename.js"/></script>

<style>标签用于为HTML文档定义样式信息

在style中,可以规定浏览器如何呈现HTML文档,标签中type属性是必须的,定义style元素的内容,唯一可能值是text/css,style元素位于head部分中。

<head>
  <style type="text/css">
    /* ... */
  </style>
</head>

<link>标签定义文档与外部资源的关系,常见的用途是连接样式表,在 HTML 中,<link> 标签没有结束标签。

<head>
<link rel="stylesheet" type="text/css" href="theme.css" />
</head>

HTML 5

初期需要了解内容:

  • 新便签在各浏览器的兼容情况
  • 与媒体相关的标签
  • HTML 5 API
  • Canvas

在后面涉及到的时候再开专题进行介绍。


个人静态博客:

查看原文

赞 0 收藏 0 评论 0

Yumiku 发布了文章 · 2019-01-30

Java核心技术笔记 异常、断言和日志

《Java核心技术 卷Ⅰ》 第7章 异常、断言和日志

  • 处理错误
  • 捕获异常
  • 使用异常机制的技巧
  • 记录日志

处理错误

如果由于出现错误而是的某些操作没有完成,程序应该:

  • 返回到一种安全状态,并让用户执行一些其他操作;或者
  • 允许用户保存所有操作,并以妥善方式终止程序

检测(或引发)错误条件的代码通常离:

  • 能让数据恢复到安全状态
  • 能保存用户的操作结果并正常退出程序

的代码很远。

异常处理的任务:将控制权从错误产生地方转移给能够处理这种情况的错误处理器

异常分类

在Java中,异常对象都是派生于Throwable类的一个实例,如果Java中内置的异常类不能满足需求,用户还可以创建自己的异常类。

Java异常层次结构:

  • Throwable

    • Error

      • ...
    • Exception

      • IOException

        • ...
      • RuntimeException

        • ...

可以看到第二层只有ErrorException

Error类层次结构描述了Java运行时系统的内部错误资源耗尽错误,应用程序不应该抛出这种类型的对象,这种内部错误的情况很少出现,出现了能做的工作也很少。

设计Java程序时,需关注Exception层次结构,这个层次又分为两个分支,RuntimeException和包含其他异常的IOException

划分两个分支的规则是:

  • 由程序错误导致的异常属于RuntimeException
  • 程序本身无问题,由于像I/O错误这类问题导致的异常属于其他异常IOException

派生于RuntimeException的异常包含下面几种情况:

  • 错误类型转换
  • 数组访问越界
  • 访问null指针

派生于IOException的异常包含下面几种情况:

  • 试图在文件尾部后面读取数据
  • 试图打开一个不存在的文件
  • 试图根据指定字符串查找Class对象,而这个字符串表示的类并不存在

Java语言规范将派生于Exception类和RuntimeException类的所有异常统称非受查(unchecked)异常,所有其他异常都是受查(checked)异常。

编译器将核查是否为所有的受查异常提供了异常处理器

声明受查异常

一个方法不仅要告诉编译器将要返回什么值,还要告诉编译器有可能发生什么错误

异常规范(exception specification):方法应该在其首部声明所可能抛出的异常。

public FileInputStream(String name) throws FileNotFoundException

如果这个方法抛出了这样的异常对象,运行时系统会开始搜索异常处理器,以便知道如何处理这个异常对象。

当然不是所有方法都需要声明异常,下面4种情况应该抛出异常:

  1. 调用一个抛出受查异常的方法时
  2. 程序运行过程中发现错误,并且利用throw语句抛出一个受查异常
  3. 程序出现错误,一般是非受查异常
  4. Java虚拟机和运行时库出现的内部错误

出现前两种情况之一,就必须告诉调用者这个方法可能的异常,因为如果没有处理器捕获这个异常,当前执行的线程就会结束

如果一个方法有多个受查异常类型,就必须在首部列出所有的异常类,异常类之间用逗号隔开:

class MyAnimation
{
  ...
  public Image loadImage(String s) throws FileNotFoundException, EOFException
  {
    ...
  }
}

但是不需要声明Java的内部错误,即从Error继承的错误。

关于子类和超类在这部分的问题:

  • 子类方法声明的受查异常不能比超类中方法声明的异常更通用(即子类能抛出更特定的异常或者根本不抛出任何异常)
  • 如果超类没有抛出任何受查异常,子类也不能

如果类中的一个方法声明抛出一个异常,而这个异常是某个特定类的实例时:

  • 这个方法可能抛出一个这个类的异常(比如IOExcetion
  • 或抛出这个类的任意一个子类的异常(比如FileNotFoundException

如何抛出异常

假设程序代码中发生了一些很糟糕的事情。

首先要决定应该抛出什么类型的异常(通过查阅已有异常类的Java API文档)。

抛出异常的语句是:

throw new EOFException();
// 或者
EOFException e = new EOFException();
throw e;

一个名为readData的方法正在读取一个首部有信息Content-length: 1024的文件,然而读到733个字符之后文件就结束了,这是一个不正常的情况,希望抛出一个异常。

String readData(Scanner in) throws EOFException
{
  ...
  while(...)
  {
    if(!in.hasNext()) // EOF encountered
    {
      if(n < len)
        throw new EOFException();
    }
    ...
  }
  return s;
}

EOFException类还有一个含有一个字符串类型参数的构造器,这个构造器可以更加细致的描述异常出现的情况。

String gripe = "Content-length:" + len + ", Received:" + n;
throw new EOFException(gripe);

对于一个已经存在的异常类,将其抛出比较容易:

  1. 找到一个合适的异常类
  2. 创建这个类的一个对象
  3. 将对象抛出

一旦抛出异常,这个方法就不可能返回到调用者,即不必为返回的默认值或错误代码担忧。

创建异常类

实际情况中,可能会遇到任何标准异常类不能充分描述的问题,这时候就应该创建自己的异常类。

需要做的只是定义一个派生于Exception的类,或者派生于Exception子类的类。

习惯上,定义的类应该包含两个构造器:

  • 一个是默认的构造器
  • 另一个是带有详细描述信息的构造器(超类ThrowabletoString方法会打印出这些信息,在调试中有很多用)
class FileFormatException extends IOException
{
  public FileFormatException() {}
  public FileFormatException(String gripe)
  {
    super(gripe);
  }
}

捕获异常

捕获异常

要想捕获一个异常,必须设置try/catch语句块。

try
{
  code
  ...
}
catch(ExceptionType e)
{
  handler for this type
}

如果try语句块中任何代码抛出了一个在catch子句中说明的异常类,那么:

  1. 程序将跳过try语句块的其余代码
  2. 程序将执行catch子句中的处理器代码

如果没有代码抛出任何异常,程序跳过catch子句。

如果方法中的任何代码抛出了一个在catch子句中没有声明的异常类型,那么这个方法就会立即退出

// 读取数据的典型代码
public void read(String filename)
{
  try
  {
    InputStream in = new FileInputStream(filename);
    int b;
    while((b = in.read()) != -1)
    {
      // process input
      ...
    }
  }
  catch(IOException exception)
  {
    exception.printStackTrace();
  }
}

read方法有可能抛出一个IOException异常,这种情况下,将跳出整个while循环,进入catch子句,并生成一个栈轨迹

还有一种选择就是什么也不做,而是将异常传递给调用者

public void read(String filename) throws IOException
{
  InputStream in = new FileInputStream(filename);
  int b;
  while((b = in.read()) != -1)
  {
    // process input
    ...
  }
}

编译器严格地执行throws说明符,如果调用了一个抛出受查异常的方法,就必须对它进行处理,或者继续传递。

两种方式哪种更好

通常,应该捕获那些知道如何处理的异常,将那些不知道怎么样处理的异常进行传递。

这个规则也有一个例外:如果编写一个覆盖超类的方法,而这个方法又没有抛出异常,那么这个方法就必须捕获方法代码中出现的每一个受查异常;并且不允许在子类的throws说明符中出现超过超类方法所列出的异常类范围。

捕获多个异常

为每个异常类型使用一个单独的catch子句:

try
{
  code
  ...
}
catch(FileNotFoundException e)
{
  handler for missing files
}
catch(UnknownHostException e)
{
  handler for unknown hosts
}
catch(IOException e)
{
  handler for all other I/O problems
}

异常对象可能包含与异常相关的信息,可以使用e.getMessage()获得详细的错误信息,或者使用e.getClass().getName()得到异常对象的实际类型。

在Java SE 7中,同一个catch子句中可以捕获多个异常类型,如果动作一样,可以合并catch子句:

try
{
  code
  ...
}
catch(FileNotFoundException | UnknownHostException e)
{
  handler for missing files and unknown hosts
}
catch(IOException e)
{
  handler for all other I/O problems
}

只有当捕获的异常类型彼此之间不存在子类关系时才需要这个特性。

再次抛出异常与异常链

catch子句中可以抛出一个异常,这样做的目的是改变异常的类型

try
{
  access the database
}
catch(SQLException e)
{
  throws new ServletException("database error:" + e.getMessage());
}

ServletException用带有异常信息文本的构造器来构造。

不过还有一种更好的处理方法,并将原始异常设置为新异常的“原因”

try
{
  access the database
}
catch(SQLException e)
{
  Throwable se = new ServletException("database error");
  se.initCause(e);
  throw se;
}

当捕获到异常时,可以使用下面这条语句重新得到原始异常:

Throwable e = se.getCause();

这样可以让用户抛出子系统中的高级异常,而不会丢失原始异常的细节。

finally子句

当代码抛出一个异常时,就会终止方法中剩余代码的处理,并退出这个方法的执行。

如果方法获得了一些本地资源,并且只有这个方法自己知道,又如果这些资源在退出方法之前必须被回收(比如数据库连接的关闭),那么就会产生资源回收问题。

一种是捕获并重新抛出所有异常,这种需要在两个地方清除所分配的资源,一个在正常代码中,另一个在异常代码中。

Java有一种更好地解决方案,就是finally子句。

不管是否有异常被捕获,finally子句的代码都会被执行。

InputStream in = new FileInputStream(...);
try
{
  // 1
  code that might throw exception
  // 2
}
catch(IOException e)
{
  // 3
  show error message
  // 4
}
finally
{
  // 5
  in.close();
}
// 6

上面的代码中,有3种情况会执行finally子句:

  1. 代码没有抛出异常,执行序列为1、2、5、6
  2. 抛出一个在catch子句中捕获的异常

    1. 如果catch子句没有抛出异常,执行序列为1、3、4、5、6
    2. 如果catch子句抛出一个异常,异常将被抛回这个方法的调用者,执行序列为1、3、5(注意没有6)
  3. 代码抛出了一个异常,但是这个异常没有被捕获,执行序列为1、5

try语句可以只有finally子句,没有catch子句。

有时候finally子句也会带来麻烦,比如清理资源时也可能抛出异常。

如果在try中发生了异常,并且被catch捕获了异常,然后在finally中进行处理资源时如果又发生了异常,那么原有的异常将会丢失,转而抛出finally中处理的异常。

这个时候的一种解决办法是用局部变量Exception ex暂存catch中的异常:

  • try中进行执行的时候加入嵌套的try/catch,并在catch中暂存ex并向上抛出
  • finally中处理资源的时候加入嵌套的try/catch,并且在catch中进行判断ex是否存在来进一步处理
InputStream in = ...;
Exception ex = null;
try
{
  try
  {
    code that might throw exception
  }
  catch(Exception e)
  {
    ex = e;
    throw e;
  }
}
finally
{
  try
  {
    in.close();
  }
  catch(Exception e)
  {
    if(ex == null)throw e;
  }
}

下一节会介绍,Java SE 7中关闭资源的处理会容易很多。

带资源的try语句

对于以下代码模式:

open a resource
try
{
  work with the resource
}
finally
{
  close the resource
}

假设资源属于一个实现了AutoCloseable接口的类,Java SE 7位这种代码提供了一个很有用的快捷方式,AutoCloseable接口有一个方法:

void close() throws Exception

带资源的try语句的最简形式为:

try(Resource res = ...)
{
  work with res
}

try块退出时,会自动调用res.close()

try(Scanner in = new Scanner(new FileInputStream("..."), "UTF-8"))
{
  while(in.hasNext())
    System.out.println(in.next());
}

这个块正常退出或存在一个异常时,都会调用in.close()方法,就好像使用了finally块一样。

还可以指定多个资源:

try(Scanner in = new Scanner(new FileInputStream("..."), "UTF-8");
  PrintWriter out = new PrintWriter("..."))
{
  while(in.hasNext())
    System.out.println(in.next().toUpperCase());
}

不论如何这个块如何退出,inout都会关闭,但是如果用常规手动编程,就需要两个嵌套的try/finally语句。

之前的close抛出异常会带来难题,而带资源的try语句可以很好的处理这种情况,原来的异常会被重新抛出,而close方法带来的异常会“被抑制”。

分析堆栈轨迹元素

堆栈轨迹(stack trace)是一个方法调用过程的列表,包含了程序执行过程中方法调用的特定位置。

可以调用Throwable类的printStackTrace方法访问堆栈轨迹的文本描述信息。

Throwable t = new Throwable();
StringWriter out = new StringWriter();
t.printStackTrace(new PrintWriter(out));
String description = out.toString();

一种更灵活的方法是使用getStackTrace方法,会得到StackTraceElement对象的一个数组,可以在程序中分析这个对象数组:

StackTraceElement[] frames = t.getStackTrace();
for(StackTraceElement frame : frames)
  analyze frame

StackTraceElement类含有能够获得文件名和当前执行的代码行号的方法,同时还含有能获得类名和方法名的方法,toString方法会产生一个格式化的字符串,其中包含所获得的信息。

静态的Thread.getAllStackTraces方法,可以产生所有线程的堆栈轨迹。

Map<Thread, StackTraceElement[]> map = Thread.getAllStackTraces();
for(Thread t : map.keySet())
{
  StackTraceElememt[] frames = map.get(t);
  analyze frames
}

java.lang.Throwable

  • Throwable(Throwable cause)
  • Throwable(String message, Throwable cause)
  • Throwable initCause(Throwable cause):将这个对象设置为“原因”,如果这个对象已经被设置为“原因”,则抛出一个异常,返回this引用。
  • Throwable getCause():获得设置为这个对象的“原因”的异常对象,如果没有则为null
  • StackTraceElement[] getStackTrace():获得构造这个对象时调用堆栈的跟踪
  • void addSuppressed(Throwable t):为这个异常增加一个抑制异常
  • Throwable[] getSuppressed():得到这个异常的所有抑制异常

java.lang.StackTraceElement

  • String getFileName()
  • int getLineNumber()
  • String getClassName()
  • String getMethodName()
  • boolean isNativeMethod():如果这个元素运行时在一个本地方法中,则返回true
  • String toString():如果存在的话,返回一个包含类名、方法名、文件名和行数的格式化字符串,如StackTraceTest.factorial(StackTraceTest.java:18)

使用异常机制的技巧

1.异常处理不能代替简单的测试

在进行一些风险操作时(比如出栈操作),应该先检测当前操作是否有风险(比如检查是否已经空栈),而不是用异常捕获来代替这个测试。

与简单的测试相比,捕获异常需要花费更多的时间,所以:只在异常情况下使用异常机制

2.不要过分细分化异常

如果可以写成一个try/catch(s)的语句,那就不要写成多个try/catch

3.利用异常层次结构

不要只抛出RuntimeException异常,应该寻找更适合的子类或创建自己的异常类。

不要只抛出Throwable异常,否则会使程序代码可读性、可维护性下降。

4.不要压制异常

在Java中,倾向于关闭异常。

public Image loadImage(String s)
{
  try
  {
    codes
  }
  catch(Exception e)
  {}
}

这样代码就可以通过编译了,如果发生了异常就会被忽略。当然如果认为异常非常重要,就应该对它们进行处理。

5.检测错误时,“苛刻”要比放任更好

6.不要羞于传递异常

有时候传递异常比捕获异常更好,让高层次的方法通知用户发生了错误,或者放弃不成功的命令更加适宜。

断言

这部分和测试相关,以后有需要的话单独开设一章进行说明。

记录日志

不要再使用System.out.println来进行记录了

使用记录日志API吧

基本日志

简单的日志记录,可以使用全局日志记录器(global logger)并调用info方法:

Logger.getGlobal().info("File->Open menu item selected");

默认情况下会显示:

May 10, 2013 10:12:15 ....
INFO: File->Open menu item selected

如果在适当的地方调用:

Logger.getGlobal().setLevel(Level.OFF);

高级日志

可以不用将所有的日志都记录到一个全局日志记录器中,也可以自定义日志记录器:

private static final Logger myLogger = Logger.getLogger("com.mycompany.myapp");

未被任何变量引用的日志记录器可能会被垃圾回收,为了避免这种情况,可以用一个静态变量存储日志记录器的一个引用。

与包名类似,日志记录器名也具有层次结构,并且层次性更强。

对于包来说,包的名字与其父包没有语义关系,但是日志记录器的父与子之间共享某些属性。

例如,如果对com.mycompany日志记录器设置了日志级别,它的子记录器也会继承这个级别。

通常有以下7个日志记录器级别Level

  • SEVERE
  • WARNING
  • INFO
  • CONFIG
  • FINE
  • FINER
  • FINEST

默认情况下,只记录前三个级别。

另外,可以使用Level.ALL开启所有级别的记录,或者使用Level.OFF关闭所有级别的记录。

对于所有的级别有下面几种记录方法:

logger.warning(message);
logger.info(message);

也可以使用log方法指定级别:

logger.log(Level.FINE, message);

如果记录为INFO或更低,默认日志处理器不会处理低于INFO级别的信息,可以通过修改日志处理器的配置来改变这一状况。

默认的日志记录将显示包含日志调用的类名和方法名,如同堆栈所显示的那样。

但是如果虚拟机对执行过程进行了优化,就得不到准确的调用信息,此时,可以调用logp方法获得调用类和方法的确切位置,这个方法的签名为:

void logp(Level l, String className, String methodName, String message)

记录日志的常见用途是记录那些不可预料的异常,可以使用下面两个方法提供日志记录中包含的异常描述内容:

if(...)
{
  IOException exception = new IOException("...");
  logger.throwing("com.mycompany.mylib.Reader", "read", exception);
  throw exception;
}

还有

try
{
  ...
}
catch(IOException e)
{
  Logger.getLogger("com.mycompany.myapp").log(Level.WARNING, "Reading image", e);
  z
}

调用throwing可以记录一条FINER级别的记录和一条以THROW开始的信息。

剩余部分暂时不做介绍,初步了解到这即可,一把要结合IDE一起来使用这个功能。如果后续的高级知识部分有需要的话会单独开设专题来介绍。

Java异常、断言和日志总结

  • 处理错误
  • 异常分类
  • 受查异常
  • 抛出异常
  • 创建异常类
  • 捕获异常
  • 再次抛出异常与异常链
  • finally子句
  • 在资源的try语句
  • 分析堆栈轨迹元素
  • 使用异常机制的技巧
  • 基本日志与高级日志

个人静态博客:

查看原文

赞 0 收藏 0 评论 0

Yumiku 发布了文章 · 2019-01-30

Java核心技术笔记 接口、lambda表达式与内部类

《Java核心技术 卷Ⅰ》 第6章 接口、lambda表达式与内部类

  • 接口
  • 接口示例
  • lambda表达式
  • 内部类

接口

接口技术,这种技术主要用来描述类具有什么功能,而并不给出每个功能的具体实现。一个类可以实现(implement)一个或多个接口,并在需要接口的地方,随时使用实现了相应接口的对象。

接口概念

在Java程序设计语言中,接口不是类,是对类的一组需求的描述,这些类要遵从接口描述的统一格式进行定义。

Arrays类中的sort方法承诺可以对对象数组进行排序,但要求满足下列条件,对象所属的类**必须实现了Comparable接口。

public interface Comparable
{
  int compareTo(Object other)
}

这就是说,任何实现Comparable接口的类都需要包含compareTo方法,并且这个方法的参数必须是一个Object对象,返回一个整型数值;比如调用x.compareTo(y)时,当x小于y时,返回一个负数;当x等于y时,返回0;否则返回一个正数。

在 Java SE 5中,Comparable接口改进为泛型类型。

public interface Comparable<T>
{
  int compareTo(T other); // 参数拥有类型T
}

例如在实现Comparable<Employee>接口类中,必须提供int compareTo(Employee other)方法。

接口中的所有方法自动地属于public,因此,在接口中声明方法时,不必提供关键字public

  • 接口可以包含多个方法
  • 接口中可以定义常量
  • 接口中不能含有实例域
  • Java SE 8 之前,不能在接口中实现方法

提供实例域和方法实现的任务应该由实现接口的那个类来完成。

在这里可以将接口看成是没有实例域的抽象类。

现在希望用Arrays类的sort方法对Employee对象数组进行排序,Employee类必须实现Comparable接口。

为了让类实现一个接口,通常需要下面两个步骤:

  • 将类声明为实现给定的接口
  • 对接口中的所有方法进行定义

将类声明为实现某个接口,使用关键字implements

class Employee implements Comparable
{
  ...
  public int compareTo(Object otherObject)
  {
    Employee other = (Employee) otherObject;
    return Double.compare(salary, other.salary);
  }
  ...
}

这里使用了静态Double.compare方法,如果第一个参数小于第二个参数,它会返回一个负值,相等返回0,否则返回一个正值。

虽然在接口声明中,没有将compareTo方法声明为publuc,这是因为接口中所有方法都自动地是public,但是,在实现接口时,必须把方法声明为public,否则编译器将认为这个方法的访问属性是包可见性,即类的默认访问。

可以为泛型Comparable接口提供一个类型参数。

class Employee implements Comparable<Employee>
{
  ...
  public int compareTo(Employee other)
  {
    return Double.compare(salary, other.salary);
  }
  ...
}

为什么不能再Employee类直接提供一个compareTo方法,而必须实现Comparable接口呢?

主要原因是Java是一种强类型(strongly type)语言,在调用方法时,编译器将会检查这个方法是否存在。

在sort方法一般会用到compareTo方法,所以编译器必须确认一定有compareTo方法,如果数组元素类实现了Comparable接口,就可以确保拥有compareTo方法。

接口的特性

接口不是类,尤其不能用new实例化接口:

x = new Comparable(...); // Error

尽管不能构造接口的对象,却能声明接口的变量:

Comparable x; // OK

接口变量必须引用实现了接口的类对象:

x = new Employee(...); // OK

也可以使用instanceof检查一个对象是否实现了某个特定的接口:

if(x instanceof Comparable) { ... }
与类的继承关系一样,接口也可以被扩展。

这里允许存在多台从具有较高通用性的接口到较高专用性的接口的链。

假设有一个称为Moveable的接口:

public interface Moveable
{
  void move(double x, double y);
}

然后,可以以它为基础扩展一个叫做Powered的接口:

public interface Powered extends Moveable
{
  double milesPerGallon();
}

虽然接口中不能包含实例域或者静态域,但是可以定义常量:

public interface Powered extends Moveable
{
  double milesPerGallon();
  double SPEED_LIMIT = 95;
  // a public static final constant
}

与接口中的方法自动设置为public一样,接口中的域被自动设为public static final

尽管每个类只能拥有一个超类,但却实现多个接口

class Employee implements Coneable, Comparable { ... }

接口与抽象类

你可能会问:为什么这些功能不能由一个抽象类实现呢?

因为使用抽象类表示通用属性存在这样的问题:每个类只能扩展于一个类,无法实现一个类实现多个接口的需求。

class Employee extends Person implements Comparable { ... }

静态方法

在 Java SE 8 中,允许在接口中增加静态方法。
虽然说这没有什么不合法的,只是这有违接口作为抽象规范的初衷。

通常的做法是将静态方法放在伴随类中。在标准库中,有成对出现的接口和实用工具类,如Collection/CollectionsPath/Paths

虽然Java库都把静态方法放到接口中也是不太可能,但是实现自己接口时,不需要为实用工具方法另外提供一个伴随类。

默认方法

可以为接口方法提供一个默认实现,必须用default修饰符标记方法。

public interface Comparable<T>
{
  default int compareTo(T other) { return 0; }
}

默认方法的一个重要用法是接口演化(interface evolution)。

Collection接口为例,这个接口作为Java的一部分已经很久了,假如很久以前提供了一个类:

public class Bag implements Collection { ... }

后来,在Java SE 8中,为这个接口增加了一个stream方法。如果stream方法不是默认方法,那么Bag类将不能编译——因为它没有实现这个新方法。

为接口增加一个非默认方法不能保证“源代码兼容”(source compatible)。

解决默认方法冲突

如果一个接口中把方法定义为默认方法,然后又在超类或另一个接口中定义了同样的方法,会发生什么情况?

解决这种二义性,Java的规则是:

  • 超类优先,如果超类自己提供了一个具体方法,同名且有相同参数类型的默认方法会被忽略
  • 接口冲突,如果一个超接口提供了一个默认方法,另一个接口提供了一个同名而且参数类型相同的方法,必须覆盖这个方法来解决冲突

着重看一下第二个规则,考虑另一个包含getName方法的接口:

interface Named
{
  default String getName()
  {
    return getClass().getName() + "_" + hashCode();
  }
}

现在有一个类同时实现了这两个接口,这个时候需要程序员来解决这个二义性,在这个实现的方法中提供一个接口的默认getName方法。

class Student implements Person, Named
{
  public String getName()
  {
    return Person.super.getName();
  }
}

就算Named接口并没有getName的默认方法,同样需要程序员去解决这个二义性问题。

上面的是两个接口的命名冲突

现在考虑另一种情况:一个类扩展了一个超类,同时实现了一个接口,并从超类和接口继承了相同的方法。

class Student extends Person implements Named { ... }

这种情况下只会考虑超类的方法,接口所有默认方法会被忽略。

接口示例

接口与回调

回调(callback),可以指出某个特定事件时应该采取的动作。

java.swing包中有一个Timer类,可以使用它在到达给定的时间间隔发送通告。

在构造定时器时,需要设置一个时间间隔,并告知定时器,达到时间间隔时需要做什么。

其中一个问题就是如何告知定时器做什么?在很多语言中,是提供一个函数名,但是,在Java标准类库中的类采用的是面向对象方法,它将某个类的对象传递给定时器,然后定时器调用这个对象的方法。

定时器需要知道调用哪一个方法,并要求传递的对象所属的类实现了java.awt.event包的ActionListener接口:

public interface ActionListener
{
  void actionPerformed(ActionEvent event);
}

当到达指定时间间隔,定时器就调用actionPerformed方法。

使用这个接口的方法:

class TimePrinter implements ActionListener
{
  public void actionPerformed(ActionEvent event)
  {
    System.out.println(...);
    ...
  }
}

其中接口方法的ActionEvent参数提供了事件的相关信息。

接下来构造类的一个对象,并传递给Timer构造器。

ActionListener listener = new TimePrinter()
Timer t = new Timer(10000, listener);
t.start(); // 启动定时器

Comparator接口

可以对一个字符串数组排序,是因为String类实现了Comparable<String>,而且String.compareTo方法可以按字典顺序比较字符串。

现在需要按长度递增的顺序对字符串进行排序,我们肯定不能对String进行修改,就算可以修改我们也不能让它用两种不同的方式实现compareTo方法。

要处理这种情况,Arrays.sort方法还有第二个版本,一个数组和一个比较器(comparator)作为参数,比较器实现了Comparator接口的类的实例。

public interface Comparator<T>
{
  int compare(T first, t second);
}

按字符串长度比较,可以定义一个实现Comparator<String>的类:

class LengthComparator implements Comparator<String>
{
  public int compare(String first, String second)
  {
    return first.length() - second.length();
  }
}

具体比较时,建立一个实例:

Comparator<String> comp = new LengthComparator();
// comp.compare(words[i], words[j])
Arrays.sort(friends, comp);

对象克隆

Cloneable接口,指示一个类提供了一个安全的clone方法。

Employee original = new Employee(...);
Employee copy = original.clone();
copy.raiseSalary(10); // no changes happen to original

clone方法是Object的一个protected方法,代码不能直接调用这个方法(指的是Object的这个方法)。

当然,只有Employee类可以克隆Employee对象,但是默认的克隆操作是浅拷贝,即并没有克隆对象中引用的其他对象

浅拷贝可能会产生问题么?这取决于具体情况:

  • 原对象和浅克隆对象共享的子对象是不可变的,那么这种共享就是安全的
  • 在对象的生命期中,子对象一直包含不变的常量,没有更改器方法会改变它,也没有方法会生成它的引用,这种情况下也是安全的

一般来说子对象都是可变的,所以需要定义clone方法来建立一个深拷贝,同时克隆所有子对象。

对于每一个类,需要确定:

  1. 默认的clone是否满足要求
  2. 是否可以在可变子对象上调用clone来修补默认clone
  3. 是否不该使用clone

实际上第3个选项是默认选项(这句话没有太读懂)。

如果选第1个或者第2个,类必须:

  1. 实现Cloneable接口
  2. 重新定义clone方法,并指定public访问修饰符

子类虽然可以访问Object受保护的clone方法,但是子类只能调用受保护的clone方法来克隆它自己的对象

必须重新定义clonepublic,才能允许所有方法克隆对象。

Cloneable接口是一组标记接口,其他接口一般确保一个类实现一个或一组特定的方法,标记接口不包含任何方法,它的唯一作用就是允许在类型查询中使用instanceof

即时clone的默认(浅拷贝)实现能够满足要求,还是需要实现Cloneable接口,将clone重新定义为public,再调用super.clone()

class Employee implements Cloneable
{
  // raise visibility level to public, change return type
  public Employee clone() throws CloneNotSupportedExcption
  {
    return (Employee) super.clone();
  }
}

与浅拷贝相比,这个clone并没有增加任何功能,只是让方法变为公有,要建立深拷贝。

class Employee implements Cloneable
{
  ...
  public Employee clone() throws CloneNotSupportedExcption
  {
    // Obejct.clone()
    Employee cloned = (Employee) super.clone();
    //clone mutable fields
    cloned.hireDay = (Date) hireDay.clone();
    return cloned;
  }
}

如果一个对象调用clone,但这个对象类没有实现Cloneable接口,Objectclone方法就会抛出一个CloneNotSupportedExcptionEmployeeDate类实现了Cloneable接口,所以不会抛出异常,但是编译器并不知道这点,所以声明异常最好还要加上捕获异常。

class Employee implements Cloneable
{
  // raise visibility level to public, change return type
  public Employee clone() throws CloneNotSupportedExcption
  {
    try
    {
      Employee cloned = (Employee) super.clone();
      ...
    }
    catch(CloneNotSupportedExcption e) { return null; }
    // 因为实现了Cloneable,所以这并不会发生
  }
}

必须当心子类的克隆

例如,一旦Employee类定义了clone,那么就可以用它来克隆Manager对象(因为在Employee类中的clone已经是public了,可以直接使用Manager.clone())。

Employeeclone一定能完成克隆Manager对象的工作么?

这取决于Manager类的域:

  • 如果是基本类型域,那没有问题
  • 如果是需要深拷贝或者不可克隆域,不能保证子类的实现者一定会修正clone方法让它正常工作

出于后者的原因,在Object类中的clone方法声明protected

lambda表达式

一种表示在将来某个时间点执行的代码块的简洁方法。

使用lambda表达式,可以用一种精巧而简洁的方式表示使用回调或变量行为的代码。

为什么引入lambda表达式

lambda表达式是一个可传递的代码块,可以在以后执行一次或多次。

之前的监听器和后面的排序比较例子的共同点是:都是把一个代码块传递到某个对象(定时器或者是sort方法),并且这个代码块会在将来某个时间调用。

lambda表达式的语法

考虑之前的按字符串长度排序例子:

first.length() - second.length()

Java是一种强类型语言,所以还要指定他们的类型:

(String first, String second)
  -> first.length() - second.length()
  // 隐式return 默认返回这个表达式的结果

这就是一个lambda表达式,一个代码块以及变量规范。

如果代码要完成的计算不止一条语句,可以像写方法一样,把代码放在{}中,并包含显式的return语句。

(String first, String second) ->
  {
    if(first.length() < second.length()) return -1;
    else if(first.length() > second.length()) return 1;
    else return 0;
  }

一些省略形式的表达:

  • 如果没有参数,仍要提供空括号
  • 如果编译器可以推导出参数类型,可以省略类型声明
  • 如果只有一个参数,并且参数类型可以推导,则可以省略小括号

需要注意的地方:

  • 不需要指定返回类型,返回类型总是由上下文推导出(一般在赋值语句里)
  • 如果表达式里只要有一个显式return,那就要确保每个分支都有一个return,否则是不合法的

函数式接口

Java中已经有很多封装代码块的接口,比如ActionListenerComparator,lambda表达式与这些接口兼容。

对于只有一个抽象方法的接口,需要这种接口的对象时,就可以提供一个lambda表达式,这种接口称为函数式接口(functional interface)。

考虑之前的Arrays.sort方法,其中第二个参数需要一个Comparator实例,函数式接口使用:

Arrays.sort(words,
  (first, second) -> first.length() - second.length());

在底层,Arrays.sort方法会接收实现了Comparator<Strng>的某个类的对象,在这个对象上调用compare方法会执行这个lambda表达式的体。

最好把lambda表达式看作一个函数,而不是一个对象,而且要接收lambda表达式可以传递到函数式接口

lambda表达式可以转换为接口,这让lambda表达式很有吸引力,具体的语法很简单:

Timer t = new Timer(10000, event ->
  {
    System.out.println(...);
    ...
  });

与使用实现了ActionListener接口的类相比,这个代码可读性好很多。

实际上,在Java中,对lambda表达式所能做的也只是能转换为函数式接口,甚至不能把lambda表达式赋给类型为Object的变量,Object不是一个函数式接口。

方法引用

有时,可能已经有现成的方法可以完成你想要传递到其他代码的某个动作。

比如只要出现一个定时器事件就打印这个事件对象:

Timer t = new Timer(10000, event -> System.out.println(event));

但是如果直接把println方法传递给Timer构造器就更好了:

Timer t = new Timer(10000, System.out::println);

表达式System.out::println就是一个方法引用(method reference),它等价于lambda表达式x - > System.out.println(x)

考虑一个排序例子:

Arrays.sort(words, String::compareToIgnoreCase);

主要有3种情况:

  • object::instanceMethod
  • Class::staticMethod
  • Class::instanceMethod

前面两种等价于提供方法参数的lambda表达式,比如System.out::println等价于x -> System.out.println(x),以及Math::power等价于(x, y) -> Math.power(x, y)

对于第3种,第1个参数会成为方法的目标,例如String::compareToIgnoreCase等价于(x, y) -> x.compareToIgnoreCase(y)

可以在方法引种中使用thissuper也是合法的,比如super::instanceMethod,使用this作为目标,会调用给定方法的超类版本。

构造器引用

构造器引用与方法引用类似,只不过方法名为new,例如Person::newPerson构造器的一个引用,具体选择Person多个构造器中的哪一个,这个取决于上下文。

现在有一个字符串列表,你可以把它转换为一个Person对象数组,为此要在各个字符串上调用构造器。

ArrayList<String> names = ...;
Stream<Person> stream = names.stream().map(Person::new);
List<Person> people = stream.collect(Collectors.toList());

streammapcollect方法会在卷Ⅱ的第1章讨论。

现在的重点是map方法会为各个列表元素调用Person(String)构造器,这里编译器从上下文推导出这是在对一个字符串调用构造器。

可以用数组类型建立构造器引用,int[]::new是一个构造器引用,有一个参数,就是数组的长度,这等价于x -> new int[x]

Java有一个限制:无法构造泛型类型T的数组。

数组构造器引用对于克服这个限制很有用。

假设需要一个Person对象数组,Stream接口有一个toArray方法可以返回Object数组:

Object[] people = stream.toArray();

但是用户想要一个Person引用数组,流库利用构造器引用解决了这个问题:

Person[] people = stream.toArray(Person[]::new);

toArray方法调用构造器获得一个正确类型的数组,然后填充这个数组并返回。

变量作用域

通常可能想在lambda表达式中访问外围方法或类中的变量

public static void repeatMessage(String text, int delay)
{
  ActionListener listener = event ->
    {
      System.out.println(text);
      ...
    };
  new Timer(delay, listener).start();
}

具体调用:

repeatMessage("Hello", 1000);

lambda表达式中的变量text,并不是在这个lambda表达式中定义的,但是这其实有问题,因为代码可能会调用返回很久以后才运行,而那时这个参数变量已经不存在了,该如何保留这个变量?

重温一下lambda表达式的3个部分:

  1. 一个代码块
  2. 参数
  3. 自由变量的值,指非参数并且不在代码中定义的变量

上面的例子中有1个自由变量text

表示lambda表达式的数据结构必须存储自由变量的值,也被叫做自由变量被lambda表达式捕获(captured)。

可以把一个lambda表达式转换为包含一个方法的对象,这样自由变量的值就会复制到这个对象的实例变量中。

关于代码块以及自由变量有一个术语:闭包(closure),Java中lambda表达式就是闭包。

在lambda表达式中,只能引用值不会改变的变量,比如下面这种就是不合法的:

public static void countDown(int start, int delat)
{
  ActionListener listener = event ->
    {
      start--; // Error: Can't mutate captured variable
      System.out.println(text);
      ...
    };
  new Timer(delay, listener).start();
}

如果在lambda表达式中改变变量,并发执行多个操作时就会不安全(具体要见第14章并发)。

另外如果在lambda表达式中引用变量,并且这个变量在外部改变,这也是不合法的:

public static void repeat(String text, int count)
{
  for(int i = 1; i <= count; i++)
  {
    ActionListener listener = event ->
      {
        System.out.println(i + ":" + text);
        // Error: Can't refer to changing i
        ...
      };
    new Timer(1000, listener).start();
  }
}

所以简单来说规则就是:lambda表达式中捕获的变量必须实际上是最终变量(effectively final),即这个变量初始化之后就不再赋新值

lambda表达式的体与嵌套块有相同的作用域,所以在lambda表达式中声明与一个局部变量同名的参数或局部变量是不合法的。

Path first = Path.get("/usr/bin");
Comparator<String> comp =
  (first, second) -> fisrt.length() - second.length();
  // Error: Variable first already defined

当然在lambda表达式中也不能有同名的局部变量。

在lambda表达式中使用this关键字时,是指创建这个lambda表达式的方法的this参数

public class Application()
{
  public void init()
  {
    ActionListener listener = event ->
      {
        System.out.println(this.toString());
        ...
      }
  }
}

this.toString()会调用Application对象的toString方法,而不是ActionListener实例的方法,所以在lambda表达式中this的使用并没有什么特殊之处。

内部类

内部类(inner class)定义在另一个类的内部,其中的方法可以访问包含它们的外部类的域。

内部类主要用于设计具有相互协作关系的类集合。

使用内部类的主要原因:

  1. 内部类方法可以访问该类定义所在的作用域中的数据,包括私有数据
  2. 内部类可以对同一个包中的其他类隐藏起来
  3. 想定义一个回调函数且不想编写大量代码时,使用匿名(anonymous)内部类比较便捷。

将从以下几部分介绍内部类:

  1. 简单的内部类,它将访问外围类的实例域
  2. 内部类的特殊语法规则
  3. 内部类的内部,探讨如何转换成常规类
  4. 讨论局部内部类,它可以访问外围作用域中的局部变量
  5. 介绍匿名内部类,说明Java在lambda表达式之前怎么实现回调的
  6. 介绍如何将静态内部类嵌套在辅助类中

内部类访问对象状态

内部类的语法比较复杂。

选择一个简单的例子:

public class TalkingClock
{
  private int interval;
  private boolean beep;

  public TalkingClock(int interval, boolean beep) { ... }
  public void start() { ... }

  // an inner class
  public class TimePrinter implements ActionListener
  {
    public void actionPerformed(ActionEvent event)
    {
      System.out.println(...);
      if(beep) Toolkit.getDefaultToolkit().beep();
    }
  }
}

TimePrinter类位于TalkingClock类内部,并不意味着每个TalkingClock对象都有一个TimePrinter实例域。

TimePrinter类没有实例域或者beep变量,而是引用了外部类的域里的beep

其实内部类的对象总有一个隐式引用,它指向了创建它的外部类对象,这个引用在内部类的定义中不可见。

这个引用是在对象创建内部类对象的时候传入的this,编译器通过内部类的构造器传入到内部类对象的域中。

// 由编译器插入的语句
ActionListener listener = new TimePrinter(this);

TimePrinter类可以声明为私有的,这样只有TalkingClock方法才能构造TimePrinter对象。只有内部类可以是私有的,常规类只可以是包可见和公有可见。

内部类的特殊语法规则

使用外围类引用的语法为OuterClass.this

例如之前的actionPerformed方法:

public void actionPerformed(ActionEvent event)
{
  ...
  if(TalkingClock.this.beep) Toolkit.getDefaultToolkit().beep();
}

反过来,可以用`outerObject.new InnerClass(construction parameters)更加明确地编写内部类对象的构造器:

// ActionListener listener = new TimePrinter(this);
ActionListener listener = this.new TimePrinter();

通常来说this限定词是多余的,但是可以通过显式命名将外围类引用设置为其他对象,比如当TimePrinter是一个公有内部类时,对于任意的语音时钟都可以构造一个TimePrinter

TalkingClock jabberer = new TalkingClock(1000, true);
TalkingClock.ActionListener listener = jabberer.new TimePrinter();

上面的情况是在外围类的作用域之外,所以引用的方法是OuterClass.InnerClass

注意:内部类中声明的所有静态域都必须是final,因为我们希望一个静态域只有一个实例。不过对于每个外部对象,会分别有一个单独的内部类实例,如果这个域不是final,它可能就不是唯一的。

局部内部类

如果一个类只在一个方法中创建了对象,可以这个方法中定义局部类。

public void start()
{
  class TimePrinter implements ActionListener
  {
    public void actionPerformed(ActionEvent event) { ... }
  }

  ActionListener listener = new TimePrinter();
  Timer t = new Timer(interval, listener);
  t.start();
}

局部类不能用publicprivate,它的作用域被限定在生命这个局部类的块中。

但是有非常好的隐蔽性,除了start方法,没有任何方法知道TimePrinter类的存在。

由外部方法访问变量

局部类还有一个优点:他们还能访问局部变量,但是这些局部变量必须是final,即一旦赋值就不会改变。

下面的例子相比之前进行了一些修改,beep不再是外部类的一个实例域,而是方法传入的参数变量:

public void start(int interval, final boolean beep)
{
  class TimePrinter implements ActionListener
  {
    public void actionPerformed(ActionEvent event)
    {
      ...
      if(beep) ...;
      ...
    }
  }

  ActionListener listener = new TimePrinter();
  Timer t = new Timer(interval, listener);
  t.start();
}

先说明一下这里的控制流程:

  1. 调用start(int, boolean)
  2. 调用局部内部类TimePrinter的构造器,初始化listener
  3. listener引用传递给Timer构造器
  4. 定时器t开始计时
  5. start(int, boolean)方法结束,此时beep参数变量不复存在
  6. 某个时刻actionPerformed方法执行if(beep) ...

为了让actionPerformed正常运行,TimePrinter类在beep域释放之前将内部类中要用到的beep域用start方法的局部变量beep进行备份(具体实现方式是编译器给内部类添加了一个final域用来保存beep)。

编译器检测对局部变量的访问,为每一个量建立相应的数据域,并将局部变量拷贝到构造器中,以便将这些数据域初始化为局部变量的副本

至于beep参数前的final,是因为局部类的方法只能引用定义为final的局部变量,从而使得局部变量与局部类中建立的拷贝保持一致。

匿名内部类

假设只创建这个局部类的一个对象,就不必命名了,这种类称为匿名内部类(anonymous inner class)。

public void start(int interval, boolean beep)
{
  ActionListener listener = new ActionListener()
  {
    public void actionPerformed(ActionEvent event) { ... }
  };
  Timer t = new Timer(interval, listener);
  t.start();
}

这种语法的含义是:创建一个实现AcitonListener接口的类的新对象,需要实现的方法定义在括号内。

通常的语法格式为:

new SuperType(construction parameters)
  {
    methods and data
  }

SuperType可以是一个接口,也可以是一个类。

由于构造器必须要有一个名字,所以匿名类不能有构造器,取而代之的是:

  • SuperType是一个超类时,将构造器参数传递给超类构造器
  • SuperType是一个接口时,不能有任何构造参数(括号()还是要保留的)

构造一个类的新对象,和构造一个扩展这个类的匿名内部类的对象的区别:

Person queen = new Person("Mary");
Person count = new Person("Dracula") { ... };

多年来,Java程序员习惯用匿名内部类实现事件监听器和其他回调,如今最好还是使用lambda表达式,比如:

public void start(int interval, boolean beep)
{
  Timer t = new Timer(interval, event -> { ... });
  t.start();
}

可见,用lambda表达式写会简洁得多。

双括号初始化

如果想构造一个数组列表,并传递到一个方法:

ArrayList<String> friends = new ArrayList<>();
friends.add("Harry");
friends.add("Tony");
invite(friends);

如果之后都没有再需要这个数组列表,那么最好使用一个匿名列表解决。

invite(new ArrayList<String>() {{ add("Harry"); add("Tony"); }};

注意这里的双括号:

  • 外层括号建立了ArrayList的一个匿名子类
  • 内层括号则是一个对象构造块(见第4章)

静态内部类

有时使用内部类只是为了把一个类隐藏在另一个类的内部,并不需要内部类引用外围类对象,为此可以将内部类声明static,取消产生的引用。

编写一个方法同时计算出最大最小值:

double min = Double.POSITIV_INFINITY;
double max = Double.NEGATIVE_INFINITY;
for(double v : values)
{
  if (min > v) min = v;
  if (max < v) max = v;
}

然而必须返回两个数值,可以顶一个包含两个值的类Pair

class Pair
{
  private double first;
  private double second;
  public Pair(double f, double s)
  {
    first = f;
    second = s;
  }
  public double getFirst() { return first; }
  public double getSecond() { return second; }
}

minmax方法可以返回一个Pair类型的对象。

class ArrayAlg
{
  public static Pair minmax(double[] values)
  {
    ...
    return new Pair(min, max);
  }
}

然后调用ArrayAlg.minmax获得最大最小值:

Pair p = ArrayAlg.minmax(data);

但是Pair是一个比较大众化的名字,容易出现名字冲突,解决的方法是将Pair定义为ArrayAlg的内部公有类,然后用ArrayAlg.Pair访问它:

ArrayAlg.Pair p = ArrayAlg.minmax(data);

不过与前面的例子不同,Pair对象不需要引用任何其他的对象,所以可以把这个内部类声明为static

class ArrayAlg
{
  public static class Pair { ... }
  ...
}

只有内部类可以声明为static,静态内部类的对象除了没有对生成它的外围类对象的引用特权外,其他与所有内部类完全一样。

在上面的例子中,必须使用静态内部类,这是因为返回的内部类对象是在静态方法minmax中构造的。

如果没有把Pair类声明为static,那么编译器将会给出错误报告:没有可用的隐式ArrayAlg类型对象初始化内部类对象。

  • 注释1:在内部类不需要访问外围类对象时,应该使用静态内部类。
  • 注释2:与常规内部类不同,静态内部类可以有静态域和方法。
  • 注释3:声明在接口中的内部类自动成为staticpublic类。

代理

代理(proxy),这是一种实现任意接口的对象。

利用代理可以在运行时创建一个实现了一组给定接口新类

这种功能只有在编译时无法确定需要实现哪个接口时才有必要使用。

对于应用程序设计人员来说,遇到的情况很少,所以先跳过,如果后面有必要再开一个专题进行说明。

Java接口、lambda表达式与内部类总结

  • 接口概念、特性
  • 接口与抽象类
  • 静态方法
  • 默认方法
  • 解决默认方法冲突
  • 接口示例
  • lambda表达式
  • 函数式接口
  • 方法引用
  • 构造器引用
  • lambda表达式变量总用域
  • 内部类
  • 局部内部类
  • 匿名内部类
  • 静态内部类

个人静态博客:

查看原文

赞 0 收藏 0 评论 0

Yumiku 发布了文章 · 2019-01-30

Java核心技术笔记 继承

《Java核心技术 卷Ⅰ》 第5章 继承

  • 类、超类、子类
  • Object:所有类的超类
  • 泛型数组列表
  • 对象包装器与自动装箱
  • 参数数量可变的方法
  • 枚举类
  • 继承的设计技巧

类、超类和子类

定义子类

关键字extend表示继承。

public class Manager extends Employee
{
  // 添加方法和域
}

extend表明正在构造的新类派生于一个已存在的类。

已存在的类称为超类(superclass)、基类(base class)或父类(parent class);
新类称为子类(subclass)、派生类(derived class)或孩子类(child class)。

子类有超类没有的功能,子类封装了更多的数据,拥有更多的功能。

所以在扩展超类定义子类时,仅需要指出子类与超类的不同之处。

覆盖方法

有时候,超类的有些方法并不一定适用于子类,为此要提供一个新的方法来覆盖(override)超类中的这个方法:

public class Manager extends Employee
{
  private double bonus;
  ...
  public double getSalary()
  {
    double baseSalary = super.getSalary();
    return baseSalary + bonus;
  }
  ...
}

这里由于Manager类的getSalary方法并不能直接访问超类的私有域

这是因为尽管子类拥有超类的所有域,但是子类没法直接获取到超类的私有部分,因为超类的私有部分只有超类自己才能够访问。而子类想要获取到私有域的内容,只能通过超类共有的接口。

而这里Employee的公有方法getSalary正是这样的一个接口,并且在调用超类方法使,要使用super关键字。

那你可能会好奇:

  • 不加super关键字不行么?
  • Employee类的getSalary方法不应该是被Manager类所继承了么?

这里如果不使用super关键字,那么在getSalary方法中调用一个getSalary方法,势必会引起无限次的调用自己。

关于superthis需要注意的是:他们并不类似,因为super不是一个对象的引用,不能将super赋给另一个对象变量,它只是一个指示编译器调用超类方法的特殊关键字。

子类构造器

public Manager(String name, double salary, int year, int month, int day)
{
  super(name, salary, year, month, day);
  bonus = 0;
}

语句super(...)是调用超类中含有对应参数的构造器。

Q1:为什么要这么做

A1:由于子类的构造器不能访问超类的私有域,所以必须利用超类的构造器对这部分私有域进行初始化。但是注意,使用super调用构造器的语句必须是子类构造器的第一条语句

Q2:一定要使用么

A2:如果子类的构造器没有显示地调用超类构造器,则将自动地调用超类默认(没有参数)的构造器;如果超类并没有不带参数构造器,并且子类构造器中也没有显示调用,则Java编译器将报告错误

Employee[] staff = new Employee[3];
staff[0] = manager;
staff[1] = new Employee(...);
staff[2] = new Employee(...);
for(Employee e : staff)
{
  System.out.println(e.getName() + "" + e.getSalary());
}

这里将e声明为Emplyee对象,但是实际上e既可以引用Employee对象,也可以引用Manager对象。

  • 当引用Employee对象时,e.getSalary()调用的是EmployeegetSalary方法
  • 当引用Manager对象时,e.getSalary()调用的是ManagergetSalary方法

虚拟机知道e实际引用的对象类型,所以能够正确地调用相应的方法。

一个对象变量可以指示多种实际类型的现象被称为多态(polymorphism)。在运行时能够自动地选择调用哪个方法的现象称为自动绑定(dynamic binding)。

继承层次

集成并不仅限于一个层次。

由一个公共超类派生出来的所有类的集合被称为继承层次(inheritance hierarchy),在继承层次中,从某个特定的类到其祖先的路径被称为该类的继承链(inheritance chain)。

Java不支持多继承,有关Java中多继承功能的实现方式,见下一章有关接口的部分。

多态

"is-a"规则的另一种表述法是置换法则,它表明程序中出现超类对象的任何地方都可以用子类对象置换

Employee e;
e = new Employee(...);
e = new Manager(...);

在Java程序设计语言中,对象变量是多态的

Manager boss = new Manager(...);
Employee[] staff = new Employee[3];
staff[0] = boss;

这个例子中,虽然staff[0]boss引用同一个对象,但是编译器将staff[0]看成Employee对象,这意味着这样调用是没有问题的:

boss.setBonus(5000); // OK

但是不能这样调用:

staff[0].setBonus(5000); // Error

这里因为staff[0]声明的类型是Employee,而setBonus不是Employee类的方法。

尽管把子类赋值给超类引用变量是没有问题的,但这并不意味着反过来也可以:

Manager m = staff[2]; // Error

如果这样赋值成功了,那么编译器将m看成是一个Manager类,在调用setBonus由于所引用的Employee类并没有该方法,从而会发生运行时错误。

理解方法调用

弄清楚如何在对象上应用方法调用非常重要。

比如有一个C类对象xC有一个方法f(args)

现在以调用x.f(args)为例,说明调用过程的详细描述:

  1. 编译器查看对象的声明类型和方法名。C类中可能有多个同名的方法,编译器将列举所有C类中名为f的方法和其超类中属性为public且名为f的方法(超类私有无法访问)。
  2. 接下来,编译器查看调用方法时提供的参数类型。如果存在一个完全匹配的f,就选择这个方法,这个过程被称为重载解析(overloading resoluton)。这个过程允许类型转换(int转double,Manager转Employee等等)。如果编译器没有找到,或者发现类型转换后,有多个方法与之匹配,就会报告一个错误。
  3. 如果是private方法,static方法、final方法或者构造器,编译器将可以准确知道应该调用哪个方法,这种称为静态绑定(static binding)。如果不是这些,那调用的方法依赖于隐式参数的实际类型,并且在运行时动态绑定,比如x.f(args)这个例子。
  4. 当程序运行时,并且动态绑定调用时,虚拟机一定调用与x所引用对象的实际类型最合适的那个类的方法。比如x实际是C类,它是D类的子类,如果C类定义了方法f(String),就直接调用;否则将在C类的超类中寻找,以此类推。简单说就是顺着继承层次从下到上的寻找方法。

如果每次调用方法都要深度/广度遍历搜索继承链,时间开销非常大。

因此虚拟机预先为每个类创建一个方法表(method table),其中列出了所有方法的签名和实际调用的方法,这样一来在真正调用时,只需要查表即可。

如果调用super.f(args),编译器将对隐式参数超类的方法表进行搜索。

之前的Employee类和Manager类的方法表:

Employee:
  getName() -> Employee.getName()
  getSalary() -> Employee.getSalary()
  getHireDay() -> Employee.getHireDay()
  raiseSalary(double) -> Employee.raiseSalary(double)

Manager:
  getName() -> Employee.getName()
  getSalary() -> Manager.getSalary()
  getHireDay() -> Employee.getHireDay()
  raiseSalary(double) -> Employee.raiseSalary(double)
  setBonus(double) -> Manager.setBonus(double)

在运行时,调用e.getSalary()的解析过程:

  • 虚拟机提取实际类型的方法表(之所以叫实际类型,是因为Employee e可以引用所有Employee类的子类,所以要确定实际引用的类型)。
  • 虚拟机搜索定义getSalary签名的类,虚拟机确定调用哪个方法。
  • 最后虚拟机调用方法。

注:在覆盖一个方法时,子类方法不能低于超类方法的可见性,特别是超类方法是public,子类覆盖方法时一定声明为public,因为经常会发生这样的错误:在声明子类方法时,因为遗漏public而使编译器把它解释为更严格的访问权限。

阻止继承:final类和方法

有时候,可能希望阻止人们利用某个类定义子类。

不允许扩展的类被称为final类。

如果在定义类时使用了final修饰符就表明这个类是final类。

public final class Executive extends Manager
{
  ...
}

方法也可以被声明为final,这样子类就不能覆盖这个方法,final类中的所有方法自动地称为final方法。

public class Employee
{
  ...
  public final String getName()
  {
    return name;
  }
  ...
}

这里注意final域的区别,final域指的是构造对象后就不再运行改变他们的值了,不过如果一个类声明为final,只有其中的方法自动地成为final,而不包括域。

将方法或类声明为final主要目的是:确保不会在子类中改变语义。

强制类型转换

有时候就像将浮点数转换为整数一样,也可能需要将某个类的对象引用转换成另一个类的对象引用。

对象引用的转换语法与数值表达式的类型转换类似,仅需要一对圆括号将目标类名括起来,并放置在需要转换的对象引用之前就可以了。

Manager boss = (Manager) staff[0];
// 因为之前把boss这个Manager类对象也存在了Employee数组中
// 现在通过强制类型转换回复成Manager类
进行类型转换的唯一原因:在暂时忽略对象的实际类型之后,使用对象的全部功能。

在Java中,每个对象变量都属于一个类型,类型描述了这个变量所引用的以及能够引用的对象类型。

将一个值存入变量时,编译器将检查是否允许该操作:

  • 将一个子类的引用赋给一个超类变量,编译器时允许的
  • 但是将一个超类引用赋给一个子类变量,必须进行类型转化,这样才能通过运行时的检查

如果试图在继承链上进行向下的类型转换,并谎报有关对象包含的内容(比如硬要把一个Employee类对象转换成Manager类对象):

Manager boss = (Manager) staff[1]; // Error

运行时,Java运行时系统将报告这个错误(不是在编译阶段),并产生一个ClassCastException异常,如果没有捕获异常,程序将会终止。

所以应该养成一个良好习惯:在进行类型强转之前,先查看一下是否能成功转换,使用instanceof操作符即可:

if(staff[1] instanceof Manager)
{
  boss = (Manager) staff[1];
  ...
}

注:如果xnull,则它对任何一个类进行instanceof返回值都是false,它因为没有引用任何对象。

抽象类

位于上层的类通常更具有通用性,甚至可能更加抽象,对于祖先类,我们通常只把它作为派生其他类的基类,而不作为想使用的特定的实例类,比如Person类对于EmployeeStudent类而言。

由于Person对子类一无所知,但是又想规范他们,一种做法是提供一个方法,然后返回空的值,另一种就是使用abstract关键字,这样Person就完全不用实现这个方法了。

public abstract String getDescription();
// no implementation required

为了提供程序的清晰度,包含一个或多个抽象方法的类本身必须被声明为抽象的。

public abstract class Person
{
  private String name;
  ...
  public abstract String getDescription();
  ...
  public String getName()
  {
    return name;
  }
}

除了抽象方法外,抽象类还可以包含具体数据和具体方法。

尽管许多人认为,在抽象类中不能包含具体方法,但是还是建议尽量把通用的域和方法(不管是否抽象)都放在超类(不管是否抽象)中。

虽然你可以声明一个抽象类的引用变量,但是只能引用非抽象子类的对象,因为抽象类不能被实例化。

在非抽象子类中定义抽象类的方法:

public class Student extends Person
{
  private String major;
  ...
  public String getDescription()
  {
    return "a student majoring in " + major;
  }
}

尽管Person类中没有具体定义getDescription的具体内容,但是当一个Person类型引用变量p使用p.getDescription()也是没有问题的,因为根据前面的方法调用过程,在运行时,方法的实际寻找是从实际类型开始寻找的,而实际类型都是定义了这个方法的具体内容。

那你可能会问,我可以只在Student类中定义getDescription不就行了么?为什么还要在Person去声明?因为如果这样的话,就不能通过p调用getDescription方法了,因为编译器只允许调用在类中声明的方法。

受保护访问

有些时候,人们希望超类中的某些方法允许被子类访问,或允许子类的方法访问超类中的某个域,而不让其他类访问到。

为此,需要将这些方法或域声明为protected

例如,如果Employee中的hireDay声明为protected,而不是private,则Manager中的方法就可以直接访问它。

不过,Manager中的方法只能够访问Manager对象中的hireDay,而不能访问其他Employee对象中的这个域,这样使得子类只能获得访问受保护域的权利。

对于受保护的域来说,但是这在一定程度上违背了OOP提倡的数据封装原则,因为如果当一个超类进行了一些修改,就必须通知所有使用这个类的程序员(而不像普通的private域,只能通过开放的方法去访问)。

相比较,受保护的方法更具有实际意义。如果需要限制一个方法的使用,就可以声明为protected,这表明子类得到信任,可以正确地使用这个方法,而其他类(非子类)不行。

这种方法的一个最好示例就是Object类中的clone方法。

归纳总结Java控制可见性的4个访问修饰符:

  1. public:对所有类可见
  2. protected:对本包和所有子类可见
  3. private:仅对本类可见
  4. 默认,无修饰符:仅对本包可见

Object:所有类的超类

Obejct类是Java中所有类的始祖,Java中每个类都是它扩展而来。

如果没有明确指出超类,Object就被认为是这个类的超类。

自然地,可以使用Object类型的变量引用任何类型的对象:

Obejct obj = new Employee("Harry Hacker", 35000);

Object类型的变量只能用于各种值的通用持有者。如果想要对其中的内容进行具体操作,还需要清楚对象的原始类型,并进行相应的类型转换:

Employee e = (Employee) obj;

equals方法

Object类中的equals方法用于检测一个对象是否等于另外一个对象。

这里的等于指的是判断两个对象是否具有相同的引用

但是在判断两个不确定是否为null的对象是否相等时,需要使用Objects.equals方法,如果两个都是null,将返回true;如果其中一个为null,另一个不是,则返回false;如果两个都不为null,则调用a.equals(b)

当然大多数时候Object.equals并不能满足,一般来说我们需要比较两个对象的状态是否相等,这个时候需要重写这个方法:

public class Manager extends Employee
{
  ...
  public boolean equals(Object otherObject)
  {
    // 首先要调用超类的equals
    if(!super.equals(otherObejct)) return false;
    Manager other = (Manager) otherObject;
    return bonus == other.bonus;
  }
}

相等测试与继承

在阅读后面的书籍笔记内容之前,首先补充一下getClassinstanceof到底是什么:

  • obejct.getClass():返回此object的运行时类Class(Java中有一个类叫Class)。比如一个Person变量p,则p.getClass()返回的就是Person这个类的Class对象,Class类提供了很多方法来获取这个类的相关信息
  • obejct instanceof ClassName:用来在运行时指出这个对象是否是这个特定类或者是它的子类的一个实例,比如manager instanceof Employee是返回true

好了,让我们回到原书吧。

如果隐式和显示的参数不属于同一个类,equals方法如何处理呢?

有许多程序员喜欢使用instanceof来进行检测:

if(!otherObject instanceof Employee) return false;

这样做不但没有解决otherObject是子类的情况,并且还可能招致一些麻烦。

Java语言规范要求equals方法具有下面的特性:

  • 自反性:任何非空引用xx.equals(x)应返回true
  • 对称性:任何引用xyy.equals(x)返回true,则x.equals(y)也应该返回true
  • 传递性:任何引用xyz,如果x.equals(y)返回truey.equals(z)返回true,则x.equals(z)也应该返回true
  • 一致性:如果xy引用对象没有发生变化,反复调用x.equals(y)应该返回同样结果
  • 任意非空引用xx.equals(null)应该返回false

从两个不同的情况看一下这个问题:

  • 如果子类能够拥有自己的相等概念,则对称性需求将强制采用getClass进行检测
  • 如果由超类决定相等的概念,那么就可以使用instanceof进行检测,这样可以在不同子类的对象之间进行相等的比较

给出一个编写完美equals方法的建议:

  1. 显示参数命名为otherObejct,稍后将它转换成另一个叫做other的变量
  2. 检测thisotherObject是否因用同一个对象:

    if(this == otherObject) return true;
  3. 检测otherObject是否为null

    if(otherObject == null) return false;
  4. 比较thisotherObject是否属于同一个类

    // 如果equals语义在每个子类中有改变,就用getClass
    if(getClass() != otherObject.getClass()) return false;
    // 如果子类拥有统一的语义,就用instanceof检测
    if(!(otherObejct instanceof ClassName)) return false;
  5. otherObejct转换为相应的类类型变量:

    ClassName other  = (ClassName) otherObejct;
  6. 开始进行域的比较,使用==比较基本类型域,使用Objects.equals比较对象域

    return field1 == other.field1
      && Objects.equals(field2, other.field2)
      && ...;

如果在子类中重新定义equals,还要在其中包含调用super.equals(other)

另外,对于数组类型的域,可以使用静态的Array.equals方法检测相应的数组元素是否相等。

hashCode方法

散列码(hash code)是由对象导出的一个整数值。

散列码是没有规律的,如果xy是两个不同的对象,x.hashCode()y.hashCode()基本上不会相同。

对于String类而言,字符串的散列码是由内容导出的。

由于hashCode方法定义在Object类中,因此每个对象都有一个默认的散列码,其值为对象的存储地址。

如果重新定义equals方法,就必须重新定义hashCode方法,以便用户可以将对象插入到散列表中。

hashCode方法应该返回一个整数数值(也可以是负数),并合理地组合实例域的散列码,以便让各个不同的对象产生的散列码更加均匀。

例如,Employee类的hashCode方法:

public class Employee
{
  public int hashCode()
  {
    return 7 * name.hashCde()
      + 11 * new Double(salary).hashCode()
      + 13* hireDay.hashCode();
  }
}

不过如果使用null安全的方法Objects.hashCode(...)就更好了,如果参数为null,这个方法返回0。

另外,使用静态方法Double.hashCode(salary)来避免创建Double对象。

还有更好的做法,需要组合多个散列值时,可以调用Objects.hash并提供多个参数。

public int hashCode()
{
  return Obejcts.hash(name, salary, hireDay);
}
equalshashCode的定义必须一致:如果x.equals(y)返回true,那么x.hashCode()就必须与y.hashCode()具有相同的值。

toString方法

Object中还有一个重要的方法,就是toString方法,它用于返回表示对象值的字符串。

绝大多数(但不是全部)的toString方法都遵循这样的格式:类的名字,随后是一对方括号括起来的域值。

public String toString()
{
  return getClass().getName()
    + "[name=" + name
    + ",salary=" + salary
    + ",hireDay=" + hireDay
    + "]";
}

toString方法也可以供子类调用。

当然,设计子类的程序员也应该定义自己的toString方法,并将子类域的描述添加进去。

如果超类使用了getClass().getName(),子类只需要调用super.toString()即可。

public class Manager extends Employee
{
  ...
  public String toString()
  {
    return super.toString()
      + "[bonus=" + bonus
      + "]";
  }
}

现在,Manager对象将打印输出如下所示内容:

Manager[name=...,salary=...,hireDay=...][bonus=...]

注意这里在子类中调用的super.toString(),不是在超类Employee中调用的么?为什么打印出来的是Manager

因为getClass正如前面所说,获取的是这个对象运行时的类,与在哪个类中调用无关。

如果任何一个对象x,调用System.out.println(x)时,println方法就会直接调用x.toString(),并打印输出得到的字符串。

Object类定义了toString方法,用来打印输出对象所属类名和散列码

System.out.println(System.out)
// 输出 java.io.PrintStream@2f6684

这样的结果是PrintStream类设计者没有覆盖toString方法。

对于一个数组而言,它继承了object类的toString方法,数组类型按照旧的格式打印:

int[] luckyNumbers = { 2, 3, 5, 7, 11, 13 };
String s = "" + luckyNumbers;
// s [I@1a46e30

前缀[I表明是一个整形数组,如果想要得到里面内容的字符串,应该使用Arrays.toString

String s = Arrays.toString(luckyNumbers);
// s [2,3,5,7,11,13]

如果想要打印多维数组,应该使用Arrays.deepToString方法。

强烈建议为自定义的每一个类增加toString方法。

泛型数组列表

在许多程序设计语言中,必须在编译时就确定整个数组大小。

在Java中,允许运行时确定数组的大小

int actualSize = ...;
Employee[] staff = new Employee[actualSize];

当然,这段代码并没有完全解决运行时动态更改数组的问题。一旦确定了大小,想要改变就不容易了。

在Java中,最简单的解决方法是使用Java中另一个被称为ArrayList的类,它使用起来有点像数组,但在添加或删除元素时,具有自动调节数组容量的功能,而不需要为此编写任何代码。

ArrayList是一个采用类型参数(type paraneter)的泛型类(generic class)。为了指定数组列表保存的元素对象类型,需要用一对尖括号将类名括起来加在后面,例如ArrayList<Employee>

ArrayList<Employee> staff = new ArrayList<Employee>();
// 两边都是用参数有些繁琐,在Java SE 7中,可以省去右边的类型参数
ArrayList<Employee> staff = new ArrayList<>();

这一般叫做“菱形语法”(<>),可以结合new操作符使用。

如果赋值给一个变量,或传递到某个方法,或者从某个方法返回,编译器会检查这个变量、参数或方法的泛型类型,然后将这个类型放在<>中。

在这个例子中,new ArrayList<>()将赋值给一个类型为ArrayList<Employee>的变量,所以泛型类型为Employee

使用add方法可以将元素添加到数组列表中。

staff.add(new Employee(...));

数组列表管理着对象引用的一个内部数组,最终数组空间有可能被用尽,这时数组列表将会自动创建一个更大的数组,并将所有的对象从较小数组中拷贝到较大数组中。

也可以确定存储的元素数量,在填充数组前调用ensureCapacity方法:

// 分配一个包含100个对象的内部数组
// 在100次调用add时不用再每次都重新分配空间
staff.ensureCapacity(100);
// 当然也可以通过把初始容量传递给构造器实现
ArrayList<Employee> staff = new ArrayList<>(100);

size方法返回数组列表包含的实际元素数目:

staff.size()

一旦能够确认数组列表大小不再发生变化,可以调用trimToSize方法。这个方法将存储区域的大小调整为当前元素数量所需要的存储空间数目,垃圾回收器将回收多余的存储空间。

访问数组列表元素

数组列表自动扩展容量的便利增加了访问元素语法的复杂程度。

需要使用getset方法实现或改变数组元素的操作,而不是[index]语法格式。

staff.set(i, harry);
Employee e = staff.get(i);

当没有泛型类时,原始的ArrayList类提供的get方法别无选择只能返回Object,因此,get方法的调用者必须对返回值进行类型转换:

Employee e = (Employee) staff.get(i);

当然还是有一个比较方便的方法来灵活扩展又方便访问:

ArrayList<X> list = new ArrayList<>();
while(...)
{
  x = ...;
  list.add(x);
}
X[] a = new X[list.size()];
// 使用toArray方法把数组元素拷贝到一个数组中
list.toArray(a);

还可以在数组列表的中间插入元素:

int n = staff.size()/2;
staff.add(n, e);

当然也可以删除一个元素:

Employee e = staff.remove(n);

可以使用for each循环遍历数组列表:

for(Employee e : staff)
  do sth with e

对象包装器与自动装箱

有时需要将int这样的基本类型转换为对象,所有基本类型都有一个与之对应的类。

例如,Integer类对应基本类型int,通常这些类称为包装器(wrapper)。

这些对象包装器有很明显的名字:IntegerLongFloatDoubleShortByteCharacterVoidBoolean(前6个类派生于公共超类Number)。

对象包装器类是不可变的,即一旦构造了包装器,就不允许更改包装在其中的值。

同时,对象包装器类还是final,因此不能定义它们的子类。

有一个很有用的特性,便于添加int类型的元素到ArrayList<Integer>中。

ArrayList<Integer> list = new ArrayList<>();
list.add(3);
// 这里将自动地变为
list.add(Integer.valueOf(3));

这种变换被称为自动装箱(autoboxing)。

相反地,将一个Integer对象赋给一个int值时,将会自动地拆箱。

int n = list.get(i);
// 将会被翻译成
int n = list.get(i).intValue();

在算术表达式中也能自动地装箱和拆箱,例如自增操作符应用于一个包装器引用:

Integer n = 3;
n++;

编译器自动地插入一条对象拆箱指令,然后自增,然后再结果装箱。

==虽然也可以用于对象包装器对象,但一般检测是对象是否指向同一个存储区域。

Integer a = 1000;
Integer b = 1000;
if(a == b) ...;

然而Java中上面的判断是有可能(may)成立的(这也太玄学了),所以解决办法一般是使用equals方法。

还有一些需要强调的:

  • 包装器引用可以为null,所以自动装箱可能会抛出NullPointerException异常
  • 如果条件表达式中混用IntegerDouble类型,Integer值就会拆箱,提升为double,再装箱为Double
  • 装箱和拆箱是编译器认可的,而不是虚拟机,编译器在生成类字节码时,插入必要的方法调用,虚拟机只是执行这些字节码(就相当于一个语法糖吧)。

使用数值对象包装器还有另外一个好处,可以将某些基本方法放置在包装器中,比如,将一个数字字符串转换成数值。

int x = Integer.parseInt(s);

参数数量可变的方法

Java SE 5以前的版本中,每个Java方法都有固定数量的参数,然而现在的版本提供了可变的参数数量调用的方法。

比如printf方法的定义:

public class PrintStream
{
  public PrintStream printf(String fmt, Object... args)
  {
    return format(fmt, args);
  }
}

这里的省略号...是Java代码的一部分,表明这个方法可以接收任意数量的对象(除fmt参数外)。

实际上,printf方法接收两个参数,一个是格式字符串,另一个是Object[]数组,其中保存着所有的参数。

编译器需要对printf的每次调用进行转换,以便将参数绑定到数组上,并在必要的时候进行自动装箱:

System.out.printf("%d %s", new Object[]{ new Integer(n), "widgets" });

用户也可以自定义可变参数的方法,并将参数指定为任意类型,甚至基本类型。

// 找出最大值
public static double max(double... values)
{
  double largest = Double.NEGATIVE_INFINITY;
  for(double v : values) if(v > largest) largest = v;
  return largest;
}

枚举类

public enum Size { SMALL, MEDIUM, LARGE, EXTRA_LARGE };
实际上这个声明定义的类型是一个类,它刚好有4个实例。

因此比较两个枚举类型值时,不需要调用equals,直接使用==就可以了。

如果需要的话,可以在枚举类型中添加一些构造器、方法和域,构造器只在构造枚举常量的时候被调用。

public enum Size
{
  SMALL("S"), MEDIUM("M"), LARGE("L"), EXTRA_LARGE("XL");

  private String abbreviation;

  private Size(String abbreviation)
  {
    this.abbreviation = abbreviation;
  }

  public String getAbbreviation()
  {
    return abbreviation;
  }
}

所有的枚举类型都是Enum类的子类,他们集成了这个类的许多方法,最有用的一个是toString,这个方法能返回枚举常量名,例如Size.SMALL.toString()返回"SMALL"

toString的逆方法是静态方法valueOf

Size s = Enum.valueOf(Size.class, "SMALL");

s设置成Size.SMALL

每个枚举类型都有一个静态的values方法,返回一个包含全部枚举值的数组。

Sizep[] values = Size.values();

ordinal方法返回enum声明中枚举常量的位置,位置从0开始技术。

反射

反射是一种功能强大且复杂的机制,使用它的主要人员是工具构造者,而不是应用程序员。

所以这部分先跳过,将会在以后一个专题单独来说明。

继承的设计技巧

  1. 将公共操作和域放在超类
  2. 不要使用受保护的域
  3. 使用继承实现"is-a"关系
  4. 除非所有继承的方法都有意义,否则不要使用继承
  5. 在覆盖方法时,不要改变预期的行为,不要偏离最初的设计想法
  6. 使用多态,而非类型信息
  7. 不要过多地使用反射

Java继承总结

  • 子类(定义、构造器、方法覆盖)
  • 继承层次
  • 多态
  • 方法调用的过程细节
  • final类和方法
  • 强制类型转换
  • 抽象类
  • protected受保护访问
  • Object所有类的超类
  • equals方法
  • 相等测试与继承
  • hashCode方法
  • toString方法
  • 泛型数组列表
  • 对象包装器与自动装箱
  • 参数数量可变的方法
  • 枚举类
  • 继承设计技巧

个人静态博客:

查看原文

赞 0 收藏 0 评论 0

Yumiku 收藏了文章 · 2019-01-29

理解OAuth2.0认证与客户端授权码模式详解

一、什么是OAuth协议

    OAuth 协议为用户资源的授权提供了一个安全又简易的标准。与以往的授权方式不同之处是 OAuth的授权不会使第三方触及到用户的帐号信息(如用户名与密码),即第三方无需使用用户的用户名与密码就可以申请获得该用户资源的授权,因此 OAuth是安全的。OAuthOpen Authorization 的简写

    OAuth 本身不存在一个标准的实现,后端开发者自己根据实际的需求和标准的规定实现。其步骤一般如下:

  1. 第三方要求用户给予授权
  2. 用户同意授权
  3. 根据上一步获得的授权,第三方向认证服务器请求令牌(token
  4. 认证服务器对授权进行认证,确认无误后发放令牌
  5. 第三方使用令牌向资源服务器请求资源
  6. 资源服务器使用令牌向认证服务器确认令牌的正确性,确认无误后提供资源

二、OAuth2.0是为了解决什么问题?

    任何身份认证,本质上都是基于对请求方的不信任所产生的。同时,请求方是信任被请求方的,例如用户请求服务时,会信任服务方。所以,身份认证就是为了解决身份的可信任问题。

    在OAuth2.0中,简单来说有三方:用户(这里是指属于服务方的用户)、服务方(如微信、微博等)、第三方应用

  1. 服务方不信任用户,所以需要用户提供密码或其他可信凭据
  2. 服务方不信任第三方应用,所以需要第三方提供自已交给它的凭据(如微信授权的code,AppID等)
  3. 用户部分信任第三方应用,所以用户愿意把自已在服务方里的某些服务交给第三方使用,但不愿意把自已在服务方的密码等交给第三方应用

三、OAuth2.0成员和授权基本流程

3.1 OAuth2.0成员

  1. Resource Owner(资源拥有者:用户)
  2. Client (第三方接入平台:请求者)
  3. Resource Server (服务器资源:数据中心)
  4. Authorization Server (认证服务器)

3.2 OAuth2.0基本流程

clipboard.png

    步骤详解:

  1. Authorization Request, 第三方请求用户授权
  2. Authorization Grant,用户同意授权后,会从服务方获取一次性用户授权凭据(如code码)给第三方
  3. Authorization Grant,第三方会把授权凭据以及服务方给它的的身份凭据(如AppId)一起交给服务方的向认证服务器申请访问令牌
  4. Access Token,认证服务器核对授权凭据等信息,确认无误后,向第三方发送访问令牌Access Token等信息
  5. Access Token,通过这个Access TokenResource Server索要数据
  6. Protected Resource,资源服务器使用令牌向认证服务器确认令牌的正确性,确认无误后提供资源

    这样服务方,一可以确定第三方得到了用户对此次服务的授权(根据用户授权凭据),二可以确定第三方的身份是可以信任的(根据身份凭据),所以,最终的结果就是,第三方顺利地从服务方获取到了此次所请求的服务
    从上面的流程中可以看出,OAuth2.0完整地解决了用户服务方第三方 在某次服务时这三者之间的信任问题

四、第三方客户端授权码模式详解

4.1 授权码模式

    客户端必须得到用户的授权(authorization grant),才能获得令牌(access token)。OAuth 2.0定义了四种授权方式:

  1. 授权码模式(authorization code
  2. 简化模式(implicit
  3. 密码模式(resource owner password credentials
  4. 客户端模式(client credentials

    授权码模式(authorization code)是功能最完整、流程最严密的授权模式。它的特点就是通过客户端的后台服务器与"服务提供商"的认证服务器进行互动。

4.2 授权码流程图及步骤

    clipboard.png

    它的步骤如下:

  1. 用户访问客户端,后者将前者导向认证服务器
  2. 用户选择是否给予客户端授权
  3. 假设用户给予授权,认证服务器将用户导向客户端事先指定的重定向URI,同时附上一个授权码
  4. 客户端收到授权码,附上早先的重定向URI,向认证服务器申请令牌。这一步是在客户端的后台的服务器上完成的,对用户不可见
  5. 认证服务器核对了授权码和重定向URI,确认无误后,向客户端发送访问令牌(access token)和更新令牌(refresh token)等

4.3 步骤详情及所需参数

4.3.1 步骤1: 客户端申请认证的URI

    包含以下参数:

  • response_type:表示授权类型,必选项,此处的值固定为"code"
  • client_id:表示客户端的ID,必选项。(如微信授权登录,此ID是APPID
  • redirect_uri:表示重定向URI,可选项
  • scope:表示申请的权限范围,可选项 state:表示客户端的当前状态,可以指定任意值,认证服务器会原封不动地返回这个值

    示例:

GET /authorize?response_type=code&client_id=s6BhdRkqt3&state=xyz
        &redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb HTTP/1.1
HTTP/1.1 Host: server.example.com

    对比网站应用微信登录:请求CODE

 https://open.weixin.qq.com/connect/qrconnect?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=SCOPE&state=STATE#wechat_redirect

4.3.2 步骤3: 认证服务器回应客户端的URI

    包含以下参数:

  • code:表示授权码,必选项。该码的有效期应该很短,通常设为10分钟,客户端只能使用该码一次,否则会被授权服务器拒绝。该码与客户端ID和重定向URI,是一一对应关系。
  • state:如果客户端的请求中包含这个参数,认证服务器的回应也必须一模一样包含这个参数。

    示例:

HTTP/1.1 302 Found
Location: https://client.example.com/cb?code=SplxlOBeZQQYbYS6WxSbIA
          &state=xyz

4.3.3 步骤4:客户端向认证服务器申请令牌的HTTP请求

    包含以下参数:

  • grant_type:表示使用的授权模式,必选项,此处的值固定为"authorization_code"。
  • code:表示上一步获得的授权码,必选项。
  • redirect_uri:表示重定向URI,必选项,且必须与A步骤中的该参数值保持一致。
  • client_id:表示客户端ID,必选项。

    示例:

POST /token HTTP/1.1
Host: server.example.com
Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code&code=SplxlOBeZQQYbYS6WxSbIA
&redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb

    对比网站应用微信登录:通过code获取access_token

https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code

4.3.4 步骤5:认证服务器发送的HTTP回复

    包含以下参数:

  • access_token:表示访问令牌,必选项。
  • token_type:表示令牌类型,该值大小写不敏感,必选项,可以是bearer类型或mac类型。
  • expires_in:表示过期时间,单位为秒。如果省略该参数,必须其他方式设置过期时间。
  • refresh_token:表示更新令牌,用来获取下一次的访问令牌,可选项。
  • scope:表示权限范围,如果与客户端申请的范围一致,此项可省略。

    示例:

 HTTP/1.1 200 OK
     Content-Type: application/json;charset=UTF-8
     Cache-Control: no-store
     Pragma: no-cache

     {
       "access_token":"2YotnFZFEjr1zCsicMWpAA",
       "token_type":"example",
       "expires_in":3600,
       "refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA",
       "example_parameter":"example_value"
     }

    从上面代码可以看到,相关参数使用JSON格式发送(Content-Type: application/json)。此外,HTTP头信息中明确指定不得缓存。

    对比网站应用微信登录:返回样例

{ 
"access_token":"ACCESS_TOKEN", 
"expires_in":7200, 
"refresh_token":"REFRESH_TOKEN",
"openid":"OPENID", 
"scope":"SCOPE",
"unionid": "o6_bmasdasdsad6_2sgVt7hMZOPfL"
}

4.4 更新令牌


    如果用户访问的时候,客户端的访问令牌access_token已经过期,则需要使用更新令牌refresh_token申请一个新的访问令牌。
    客户端发出更新令牌的HTTP请求,包含以下参数:

  • granttype:表示使用的授权模式,此处的值固定为"refreshtoken",必选项。
  • refresh_token:表示早前收到的更新令牌,必选项。
  • scope:表示申请的授权范围,不可以超出上一次申请的范围,如果省略该参数,则表示与上一次一致。

    示例:

POST /token HTTP/1.1
Host: server.example.com
Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
Content-Type: application/x-www-form-urlencoded

grant_type=refresh_token&refresh_token=tGzv3JOkF0XG5Qx2TlKWIA

(完)

参考文档一:理解OAuth 2.0
参考文档二:Oauth2.0原理
OAuth 授权的工作原理是怎样的?足够安全吗?

查看原文