沉默王二

沉默王二 查看完整档案

洛阳编辑  |  填写毕业院校原创公号「沉默王二」  |  作者 编辑 www.itwanger.com 编辑
编辑

《Web全栈开发进阶之路》作者
公众号:沉默王二
微信:qing_geee
专注于有趣的Java技术,有益的程序人生

个人动态

沉默王二 发布了文章 · 今天 07:57

太厉害了,这款开源类库可以帮你简化每一行代码

“黑铁时代”读者群里有个小伙伴感慨说,“Hutool 这款开源类库太厉害了,基本上该有该的工具类,它里面都有。”讲真的,我平常工作中也经常用 Hutool,它确实可以帮助我们简化每一行代码,使 Java 拥有函数式语言般的优雅,让 Java 语言变得“甜甜的”。

但是呢,群里还有一部分小伙伴表示还不知道这个开源类库,第一次听说。所以我决定写一篇文章普及下,毕竟好的轮子值得推荐啊。

Hutool 的作者在官网上说,Hutool 是 Hu+tool 的自造词(好像不用说,我们也能猜得到),“Hu”用来致敬他的“前任”公司,“tool”就是工具的意思,谐音就有意思了,“糊涂”,寓意追求“万事都作糊涂观,无所谓失,无所谓得”(一个开源类库,上升到了哲学的高度,作者厉害了)。

看了一下开发团队的一个成员介绍,一个 Java 后端工具的作者竟然爱前端、爱数码,爱美女,嗯嗯嗯,确实“难得糊涂”(手动狗头)。

就连向这个开源类库提交的 PR(pull request)规范都非常“病态化”(哈哈哈):

废话就说到这,来吧,实操走起!

01、引入 Hutool

Maven 项目只需要在 pom.xml 文件中添加以下依赖即可。

<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>5.4.3</version>
</dependency>

Hutool 的设计思想是尽量减少重复的定义,让项目中的 util 包尽量少。一个好的轮子可以在很大程度上避免“复制粘贴”,从而节省我们开发人员对项目中公用类库和公用工具方法的封装时间。同时呢,成熟的开源库也可以最大限度的避免封装不完善带来的 bug。

就像作者在官网上说的那样:

  • 以前,我们打开搜索引擎 -> 搜“Java MD5 加密” -> 打开某篇博客 -> 复制粘贴 -> 改改,变得好用些
有了 Hutool 以后呢,引入 Hutool -> 直接 SecureUtil.md5()

Hutool 对不仅对 JDK 底层的文件、流、加密解密、转码、正则、线程、XML等做了封装,还提供了以下这些组件:

非常多,非常全面,鉴于此,我只挑选一些我喜欢的来介绍下(偷偷地告诉你,我就是想偷懒)。

02、类型转换

类型转换在 Java 开发中很常见,尤其是从 HttpRequest 中获取参数的时候,前端传递的是整形,但后端只能先获取到字符串,然后再调用 parseXXX() 方法进行转换,还要加上判空,很繁琐。

Hutool 的 Convert 类可以简化这个操作,可以将任意可能的类型转换为指定类型,同时第二个参数 defaultValue 可用于在转换失败时返回一个默认值。

String param = "10";
int paramInt = Convert.toInt(param);
int paramIntDefault = Convert.toInt(param, 0);

把字符串转换成日期:

String dateStr = "2020年09月29日";
Date date = Convert.toDate(dateStr);

把字符串转成 Unicode:

String unicodeStr = "沉默王二";
String unicode = Convert.strToUnicode(unicodeStr);

03、日期时间

JDK 自带的 Date 和 Calendar 不太好用,Hutool 封装的 DateUtil 用起来就舒服多了!

获取当前日期:

Date date = DateUtil.date();

DateUtil.date() 返回的其实是 DateTime,它继承自 Date 对象,重写了 toString() 方法,返回 yyyy-MM-dd HH:mm:ss 格式的字符串。

有些小伙伴是不是想看看我写这篇文章的时间,输出一下给大家看看:

System.out.println(date);// 2020-09-29 04:28:02

字符串转日期:

String dateStr = "2020-09-29";
Date date = DateUtil.parse(dateStr);

DateUtil.parse() 会自动识别一些常用的格式,比如说:

  • yyyy-MM-dd HH:mm:ss
  • yyyy-MM-dd
  • HH:mm:ss
  • yyyy-MM-dd HH:mm
  • yyyy-MM-dd HH:mm:ss.SSS

还可以识别带中文的:

  • 年月日时分秒

格式化时间差:

String dateStr1 = "2020-09-29 22:33:23";
Date date1 = DateUtil.parse(dateStr1);

String dateStr2 = "2020-10-01 23:34:27";
Date date2 = DateUtil.parse(dateStr2);

long betweenDay = DateUtil.between(date1, date2, DateUnit.MS);

// 输出:2天1小时1分4秒
String formatBetween = DateUtil.formatBetween(betweenDay, BetweenFormater.Level.SECOND);

星座和属相:

// 射手座
String zodiac = DateUtil.getZodiac(Month.DECEMBER.getValue(), 10);
// 蛇
String chineseZodiac = DateUtil.getChineseZodiac(1989);

04、IO 流相关

IO 操作包括读和写,应用的场景主要包括网络操作和文件操作,原生的 Java 类库区分字符流和字节流,字节流 InputStream 和 OutputStream 就有很多很多种,使用起来让人头皮发麻。

Hutool 封装了流操作工具类 IoUtil、文件读写操作工具类 FileUtil、文件类型判断工具类 FileTypeUtil 等等。

BufferedInputStream in = FileUtil.getInputStream("hutool/origin.txt");
BufferedOutputStream out = FileUtil.getOutputStream("hutool/to.txt");
long copySize = IoUtil.copy(in, out, IoUtil.DEFAULT_BUFFER_SIZE);

在 IO 操作中,文件的操作相对来说是比较复杂的,但使用频率也很高,几乎所有的项目中都躺着一个叫 FileUtil 或者 FileUtils 的工具类。Hutool 的 FileUtil 类包含以下几类操作:

  • 文件操作:包括文件目录的新建、删除、复制、移动、改名等
  • 文件判断:判断文件或目录是否非空,是否为目录,是否为文件等等
  • 绝对路径:针对 ClassPath 中的文件转换为绝对路径文件
  • 文件名:主文件名,扩展名的获取
  • 读操作:包括 getReader、readXXX 操作
  • 写操作:包括 getWriter、writeXXX 操作

顺带说说 classpath。

在实际编码当中,我们通常需要从某些文件里面读取一些数据,比如配置文件、文本文件、图片等等,那这些文件通常放在什么位置呢?

放在项目结构图中的 resources 目录下,当项目编译后,会出现在 classes 目录下。对应磁盘上的目录如下图所示:

当我们要读取文件的时候,我是不建议使用绝对路径的,因为操作系统不一样的话,文件的路径标识符也是不一样的。最好使用相对路径。

假设在 src/resources 下放了一个文件 origin.txt,文件的路径参数如下所示:

FileUtil.getInputStream("origin.txt")

假设文件放在 src/resources/hutool 目录下,则路径参数改为:

FileUtil.getInputStream("hutool/origin.txt")

05、字符串工具

Hutool 封装的字符串工具类 StrUtil 和 Apache Commons Lang 包中的 StringUtils 类似,作者认为优势在于 Str 比 String 短,尽管我不觉得。不过,我倒是挺喜欢其中的一个方法的:

String template = "{},一枚沉默但有趣的程序员,喜欢他的文章的话,请微信搜索{}";
String str = StrUtil.format(template, "沉默王二", "沉默王二");
// 沉默王二,一枚沉默但有趣的程序员,喜欢他的文章的话,请微信搜索沉默王二

06、反射工具

反射机制可以让 Java 变得更加灵活,因此在某些情况下,反射可以做到事半功倍的效果。Hutool 封装的反射工具 ReflectUtil 包括:

  • 获取构造方法
  • 获取字段
  • 获取字段值
  • 获取方法
  • 执行方法(对象方法和静态方法)
package com.itwanger.hutool.reflect;

import cn.hutool.core.util.ReflectUtil;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;

/**
 * @author 微信搜「沉默王二」,回复关键字 PDF
 */
public class ReflectDemo {
    private int id;

    public ReflectDemo() {
        System.out.println("构造方法");
    }

    public void print() {
        System.out.println("我是沉默王二");
    }

    public static void main(String[] args) throws IllegalAccessException {
        // 构建对象
        ReflectDemo reflectDemo = ReflectUtil.newInstance(ReflectDemo.class);

        // 获取构造方法
        Constructor[] constructors = ReflectUtil.getConstructors(ReflectDemo.class);
        for (Constructor constructor : constructors) {
            System.out.println(constructor.getName());
        }

        // 获取字段
        Field field = ReflectUtil.getField(ReflectDemo.class, "id");
        field.setInt(reflectDemo, 10);
        // 获取字段值
        System.out.println(ReflectUtil.getFieldValue(reflectDemo, field));

        // 获取所有方法
        Method[] methods = ReflectUtil.getMethods(ReflectDemo.class);
        for (Method m : methods) {
            System.out.println(m.getName());
        }

        // 获取指定方法
        Method method = ReflectUtil.getMethod(ReflectDemo.class, "print");
        System.out.println(method.getName());


        // 执行方法
        ReflectUtil.invoke(reflectDemo, "print");
    }
}

07、压缩工具

在 Java 中,对文件、文件夹打包压缩是一件很繁琐的事情,Hutool 封装的 ZipUtil 针对 java.util.zip 包做了优化,可以使用一个方法搞定压缩和解压,并且自动处理文件和目录的问题,不再需要用户判断,大大简化的压缩解压的复杂度。

ZipUtil.zip("hutool", "hutool.zip");
File unzip = ZipUtil.unzip("hutool.zip", "hutoolzip");

08、身份证工具

Hutool 封装的 IdcardUtil 可以用来对身份证进行验证,支持大陆 15 位、18 位身份证,港澳台 10 位身份证。

String ID_18 = "321083197812162119";
String ID_15 = "150102880730303";

boolean valid = IdcardUtil.isValidCard(ID_18);
boolean valid15 = IdcardUtil.isValidCard(ID_15);

09、扩展 HashMap

Java 中的 HashMap 是强类型的,而 Hutool 封装的 Dict 对键的类型要求没那么严格。

Dict dict = Dict.create()
        .set("age", 18)
        .set("name", "沉默王二")
        .set("birthday", DateTime.now());

int age = dict.getInt("age");
String name = dict.getStr("name");

10、控制台打印

本地编码的过程中,经常需要使用 System.out 打印结果,但是往往一些复杂的对象不支持直接打印,比如说数组,需要调用 Arrays.toString。Hutool 封装的 Console 类借鉴了 JavaScript 中的 console.log(),使得打印变成了一个非常便捷的方式。

/**
 * @author 微信搜「沉默王二」,回复关键字 PDF
 */
public class ConsoleDemo {
    public static void main(String[] args) {
        // 打印字符串
        Console.log("沉默王二,一枚有趣的程序员");

        // 打印字符串模板
        Console.log("洛阳是{}朝古都",13);

        int [] ints = {1,2,3,4};
        // 打印数组
        Console.log(ints);
    }
}

11、字段验证器

做 Web 开发的时候,后端通常需要对表单提交过来的数据进行验证。Hutool 封装的 Validator 可以进行很多有效的条件验证:

  • 是不是邮箱
  • 是不是 IP V4、V6
  • 是不是电话号码
  • 等等

Validator.isEmail("沉默王二");
Validator.isMobile("itwanger.com");

12、双向查找 Map

Guava 中提供了一种特殊的 Map 结构,叫做 BiMap,实现了一种双向查找的功能,可以根据 key 查找 value,也可以根据 value 查找 key,Hutool 也提供这种 Map 结构。

BiMap<String, String> biMap = new BiMap<>(new HashMap<>());
biMap.put("wanger", "沉默王二");
biMap.put("wangsan", "沉默王三");

// get value by key
biMap.get("wanger");
biMap.get("wangsan");

// get key by value
biMap.getKey("沉默王二");
biMap.getKey("沉默王三");

在实际的开发工作中,其实我更倾向于使用 Guava 的 BiMap,而不是 Hutool 的。这里提一下,主要是我发现了 Hutool 在线文档上的一处错误,提了个 issue(从中可以看出我一颗一丝不苟的心和一双清澈明亮的大眼睛啊)。

13、图片工具

Hutool 封装的 ImgUtil 可以对图片进行缩放、裁剪、转为黑白、加水印等操作。

缩放图片:

ImgUtil.scale(
        FileUtil.file("hutool/wangsan.jpg"),
        FileUtil.file("hutool/wangsan_small.jpg"),
        0.5f
);

裁剪图片:

ImgUtil.cut(
        FileUtil.file("hutool/wangsan.jpg"),
        FileUtil.file("hutool/wangsan_cut.jpg"),
        new Rectangle(200, 200, 100, 100)
);

添加水印:

ImgUtil.pressText(//
        FileUtil.file("hutool/wangsan.jpg"),
        FileUtil.file("hutool/wangsan_logo.jpg"),
        "沉默王二", Color.WHITE,
        new Font("黑体", Font.BOLD, 100),
        0,
        0,
        0.8f
);

趁机让大家欣赏一下二哥帅气的真容。

14、配置文件

众所周知,Java 中广泛应用的配置文件 Properties 存在一个特别大的诟病:不支持中文。每次使用时,如果想存放中文字符,就必须借助 IDE 相关插件才能转为 Unicode 符号,而这种反人类的符号在命令行下根本没法看。

于是,Hutool 的 Setting 运用而生。Setting 除了兼容 Properties 文件格式外,还提供了一些特有功能,这些功能包括:

  • 各种编码方式支持
  • 变量支持
  • 分组支持

先整个配置文件 example.setting,内容如下:

name=沉默王二
age=18

再来读取和更新配置文件:

/**
 * @author 微信搜「沉默王二」,回复关键字 PDF
 */
public class SettingDemo {
    private final static String SETTING = "hutool/example.setting";
    public static void main(String[] args) {
        // 初始化 Setting
        Setting setting = new Setting(SETTING);

        // 读取
        setting.getStr("name", "沉默王二");

        // 在配置文件变更时自动加载
        setting.autoLoad(true);

        // 通过代码方式增加键值对
        setting.set("birthday", "2020年09月29日");
        setting.store(SETTING);
    }
}

15、日志工厂

Hutool 封装的日志工厂 LogFactory 兼容了各大日志框架,使用起来也非常简便。

/**
 * @author 微信搜「沉默王二」,回复关键字 PDF
 */
public class LogDemo {
    private static final Log log = LogFactory.get();

    public static void main(String[] args) {
        log.debug("难得糊涂");
    }
}

先通过 LogFactory.get() 自动识别引入的日志框架,从而创建对应日志框架的门面 Log 对象,然后调用 debug()info() 等方法输出日志。

如果不想创建 Log 对象的话,可以使用 StaticLog,顾名思义,一个提供了静态方法的日志类。

StaticLog.info("爽啊 {}.", "沉默王二的文章");

16、缓存工具

CacheUtil 是 Hutool 封装的创建缓存的快捷工具类,可以创建不同的缓存对象:

  • FIFOCache:先入先出,元素不停的加入缓存直到缓存满为止,当缓存满时,清理过期缓存对象,清理后依旧满则删除先入的缓存。
Cache<String, String> fifoCache = CacheUtil.newFIFOCache(3);
fifoCache.put("key1", "沉默王一");
fifoCache.put("key2", "沉默王二");
fifoCache.put("key3", "沉默王三");
fifoCache.put("key4", "沉默王四");

// 大小为 3,所以 key3 放入后 key1 被清除
String value1 = fifoCache.get("key1");
  • LFUCache,最少使用,根据使用次数来判定对象是否被持续缓存,当缓存满时清理过期对象,清理后依旧满的情况下清除最少访问的对象并将其他对象的访问数减去这个最少访问数,以便新对象进入后可以公平计数。
Cache<String, String> lfuCache = CacheUtil.newLFUCache(3);

lfuCache.put("key1", "沉默王一");
// 使用次数+1
lfuCache.get("key1");
lfuCache.put("key2", "沉默王二");
lfuCache.put("key3", "沉默王三");
lfuCache.put("key4", "沉默王四");

// 由于缓存容量只有 3,当加入第 4 个元素的时候,最少使用的将被移除(2,3被移除)
String value2 = lfuCache.get("key2");
String value3 = lfuCache.get("key3");
  • LRUCache,最近最久未使用,根据使用时间来判定对象是否被持续缓存,当对象被访问时放入缓存,当缓存满了,最久未被使用的对象将被移除。
Cache<String, String> lruCache = CacheUtil.newLRUCache(3);

lruCache.put("key1", "沉默王一");
lruCache.put("key2", "沉默王二");
lruCache.put("key3", "沉默王三");
// 使用时间近了
lruCache.get("key1");
lruCache.put("key4", "沉默王四");

// 由于缓存容量只有 3,当加入第 4 个元素的时候,最久使用的将被移除(2)
String value2 = lruCache.get("key2");
System.out.println(value2);

17、加密解密

加密分为三种:

  • 对称加密(symmetric),例如:AES、DES 等
  • 非对称加密(asymmetric),例如:RSA、DSA 等
  • 摘要加密(digest),例如:MD5、SHA-1、SHA-256、HMAC 等

Hutool 针对这三种情况都做了封装:

  • 对称加密 SymmetricCrypto
  • 非对称加密 AsymmetricCrypto
  • 摘要加密 Digester

快速加密工具类 SecureUtil 有以下这些方法:

1)对称加密

  • SecureUtil.aes
  • SecureUtil.des

2)非对称加密

  • SecureUtil.rsa
  • SecureUtil.dsa

3)摘要加密

  • SecureUtil.md5
  • SecureUtil.sha1
  • SecureUtil.hmac
  • SecureUtil.hmacMd5
  • SecureUtil.hmacSha1

只写一个简单的例子作为参考:

/**
 * @author 微信搜「沉默王二」,回复关键字 PDF
 */
public class SecureUtilDemo {
    static AES aes = SecureUtil.aes();
    public static void main(String[] args) {
        String encry = aes.encryptHex("沉默王二");
        System.out.println(encry);
        String oo = aes.decryptStr(encry);
        System.out.println(oo);
    }
}

18、其他类库

Hutool 中的类库还有很多,尤其是一些对第三方类库的进一步封装,比如邮件工具 MailUtil,二维码工具 QrCodeUtil,Emoji 工具 EmojiUtil,小伙伴们可以参考 Hutool 的官方文档:https://www.hutool.cn/

项目源码地址:https://github.com/looly/hutool

PS:需要 Java 书单的话,我在 GitHub 上发现了一个宝藏项目,里面的书单可谓应有尽有。需要的小伙伴可以按需自取,地址如下所示:

https://github.com/itwanger/JavaBooks

最后,日常求个赞吧,满满的干货,我先干为敬,你随意😑

查看原文

赞 1 收藏 1 评论 0

沉默王二 发布了文章 · 10月18日

自学编程,看书还是视频?

题目是一个读者问我的,拖了很久没有回复他,因为我觉得,成年人,没得选,两个都要嘛。

但这样的回答,很难得到读者的认可,我自己也觉得略显敷衍,于是就拖啊拖,一直拖了快两个月,终于,利用假期的时间。我想清楚了,觉得答案能够拿得出手了,你们来鉴定下。

01、书,有什么好处呢?

前提条件先说一下,烂书除外。

第一,书籍比较全面,系统化,可以针对一个技术点、一门语言,循序渐进,深度挖掘,旁征博引。

第二,书和书之间可以形成互补

如果是学习 Java 的话,推荐先看《Java 核心技术卷 1》,再看《Java 编程思想》,虽然知识点是重复的,但作者的出发点是不一样的,前者认为你就是一名零基础的小白,后者认为你是有了一些编程基础的小白。

结合起来看,两本书的效果就都达到了。

第三,书籍可以引发读者的思考

视频是动态的,连续的,给我们思考的时间很少。拿周星驰和王家卫的电影来说,前者的电影就卖座,大家喜欢看,不管是不是无厘头的恶搞;后者的电影烧脑,得去思考,但一思考,情节就错过了。

书是静止的,主动权在读者手里,你想快进,就一目十行,甚至跳过去,你想细细的品味,就慢下来,咬文嚼字。视频当然也可以快进、倍速,但就失去了那个味,感觉是在打发时间而不是在学习。

第四,书籍可以反复看

如果哪一个知识点没有掌握,可以在书里面打个记号,然后反复的看,再去查找一些资料作为辅助,整个大脑对这个知识点的印象就会更深刻。视频当然也可以反复看,但操作的难度相对较大,除非是某些经典的,藏在硬盘里的。

第四,看书不费眼

我本人近视,但说实话,不是看书看的,而是因为盯着电脑屏幕或者手机屏幕时间太久导致的。为了缓解眼部疲劳,我就会选择看书,看书能够让我得到全身心的放松。

02、视频,有什么好处呢?

前提条件先说一下,烂视频除外。

第一,视频直观,能够引领读者的注意力,仿佛身临其境一般。

如果是编程方面的视频,讲师感染力强的话,能够让我们的学习效率提高很多。我当年学习编程就看了很多李兴华老师的视频,那真的叫一个舒服,节奏把握得很到位,智能 ABC 输入法用得那叫一个行云流水,导致有一段时间我都把输入法从搜狗切换到了智能 ABC,结果发现自己驾驭不了。

第二,视频更富有表达力

视频上有字幕,有画面,有声音,带给人的观感是全方位的,这一点是书没法比的。

文字到画面,画面再到视频,这是时代的进步,也是科技的体现,视频显然更符合新时代观众的口味。这也是为什么,书籍的受众在减少,而视频的受众在扩大的真实原因。

视频从本质上来说,不过是文字的一种载体而已,但现如今,生活节奏很快,社会压力很大,人们学习的时间变得越来越少,而视频,能够让我们的学习时间降到最低。

03、书,有什么缺点呢?

编程方面的书,普遍有一个缺点,就是枯燥,一本《算法导论》能让我看上十年。为什么?除了厚实,每次看,我都想睡觉,尤其是夜里睡不着的时候,看上一页,睡意就悄然袭来。哪怕是,出版社的宣传页上明目张胆地写着“风趣幽默,像读王小波的小说一样”,但也只是“像”啊(我自己的那本)。

一些翻译的书,像《Effective Java》,你到豆瓣上看看评论就能发现,大部分都在批评译者,“书是好书,能打五分,但译者的水平,只能让我给这本书打一分。”

作者很无辜,毕竟只是个技术人员,没有写小说的技巧,无论是从整体架构上,还是细节的处理上,能把技术讲清楚,讲透彻,就已经很不容易了。

译者也很无辜,毕竟有些译者就不搞技术,翻译的过程中难免出一些差错,直译的比较多,意译的很少,再加上出版社会催稿,催得多了,译者就很难做到“精益求精”。

04、视频,有什么缺点呢?

我有时候挺怀疑的,视频号只有一分钟,竟然还有人讲道理,讲技术,难不成一分钟的提炼真的能把道理讲得通,把技术讲到位。说句实在话,我看视频号就是用来消磨时间的,逗我开心一下,乐呵一下,我觉得就行了。

相对来说,B 站上的视频质量高很多,我最喜欢看的就是 15 分钟左右的视频,前后逻辑很强,该讲的知识点都能覆盖到,还能够看到 up 主的实战演示,至于 up 主本身漂不漂亮,帅不帅,还真的是次要的。

有时间的话,少刷抖音,少刷视频号,不如到 B 站的知识区学习一下。尽量不要倍速看视频,本身视频的节奏就很快,如果再倍速,大脑根本就没有思考的时间。换句话说,如果一个视频你是用倍速去看的,在一定程度上,这个视频可以读作 laji。

05、总结

在我看来,看视频就好像是跟着老师上课,看书就好像上完课后的自习,两者应该是相辅相成的。

跟着老师上课的好处,就是,老师能够把书本上重点抽离出来,帮我们按照他的思路分门别类,省去学那些不是重点知识的时间。

自习呢,能够让我们更加主动,总结出自己的学习方法,主动性就强很多,而自学的能力对于一个人来说,伴随一生,非常重要!

不管是看书还是看视频,还有一环必须加上,就是——实战

书看再多遍,视频看再多,如果不去实战,永远都是思想上的巨人,行动上的弱者。

这就好像不管是上课还是自学,最终要靠成绩说话,卷子总要是自己去做啊,只有在一张试卷做完再做完下一张的情况下,不断总结自己作战的经验,才能把书本上和视频上的知识变成是自己的,对吧?

最后,我还是要说一句,如果你无法从书籍、视频上吸收知识,要么是因为书和视频很烂,要么是学习方法不得当,多来知乎提问题交流交流就对了!

PS:喜欢看书的小伙伴,可以关注一下这个 GitHub,基本上学 Java 方面的电子书都有了,但不限于 Java,还有工具、框架、数据库、并发编程、底层、性能优化、设计模式、计算机网络、操作系统、数据结构与算法、面试、大数据、架构等等方面,应有尽有。

https://github.com/itwanger/JavaBooks

查看原文

赞 3 收藏 0 评论 0

沉默王二 发布了文章 · 10月12日

拜托,别再问我怎么自学 Java 了!和盘托出

假如有那么残酷的一天,我不小心喝错了一瓶药,一下子抹掉了我这十多年的编程经验,把我变成了一只小白。我想自学 Java,并且想要找到一份工作,我预计需要 6 个月的时间,前提条件是每天都处于高效率的学习状态当中,并且每天的学习时间至少在 12 个小时以上。

即便是这样,我敢肯定,找到的工作肯定不会太好,勉强能够维持生活吧,毕竟是零基础入门啊。

如果想更进一步,真正成为一名不可或缺的高级 Java 工程师,时间需要更久,两年、三年、五年,直到秃的那天。

想着想着,我就觉得有必要为那一天做点准备,以备不时之需。

01、第一个阶段,环境和工具准备

  • 准备一台电脑,要能联网
  • 下载、安装 JDK,配置 Java 开发环境
  • 下载、配置 Maven
  • 下载、安装 IntelliJ IDEA
  • 准备一个 GitHub 仓库(或者码云),管理 Java 源代码

Java 是一门计算机编程语言,学它的话,连台电脑都没有,学个屁。我有个亲戚家的孩子想学编程,就只看书,家里连台电脑都不配,说什么“先打好理论基础,再实操”,我真的是有点醉。

有了电脑,还得联网,自学的过程中肯定会遇到很多问题,遇到问题的时候先问搜索引擎,推荐谷歌和必应;实在没有答案的话,也可以来找我,申请加入技术交流群,问问群里面的大佬们。

既然要学 Java,JDK 是必须要先安装的,否则 Java 程序就没法编译和执行。

Maven 也是需要提前安装和配置的,因为后面进阶的话,需要一些练手项目,它们通常都需要 Maven 来加载第三方类库。

使用集成开发环境 IntelliJ IDEA 来敲 Java 代码吧,比 Eclipse 更流行。千万不要使用记事本编写源代码了,对于小白来说,时间是宝贵的,记事本只适合大牛们用来装逼,不适合小白用来编程(入门),纯浪费时间。

有了 IDEA,后面学习源码的话,就会方便很多,包括反编译字节码。

如果英语功底不太好的话,建议安装这两款 IDEA 插件:chinese 和 translation

如果注重编码规范的话,建议安装这两款 IDEA 插件:Alibaba 和 SonarLint

为什么还需要 GitHub 仓库或者码云仓库呢?它们可以用来在线云同步源代码,防止版本丢失。学到最后,还可以形成一套自己的工具库,轮子就有了,上班的时候工作效率就会高很多,能直接用的代码再也不用重新写了。

02、第二个阶段,Java 基础入门

1)基本数据类型

2)操作符

  • 算术运算符
  • 逻辑运算符
  • 比较运算符

3)流程控制语句

  • 条件分支(if/else/else if、三元运算符、switch
  • 循环或者遍历(for、while、do-while)
  • break 和 continue

4)包

  • 创建包
  • 导入包
  • 包全名

5)main 方法详解

  • public 关键字
  • static 关键字
  • void 关键字
  • main 方法
  • 字符串数组参数(String[] args

6)数组

7)注释

8)字符串

03、第三个阶段,Java 核心技术

1)面向对象

2)常用工具类

  • 字符串相关的工具类
  • 日期时间相关的工具类
  • 枚举
  • 随机数
  • 正则表达式
  • Apache-commons 工具库
  • Guava 工具库

3)集合框架

4)反射机制

  • 什么是反射?
  • 反射有什么用?
  • Class 类

5)异常处理

  • 为什么需要异常处理机制?
  • Error 和 Exception
  • try-catch-finally
  • try-with-resource
  • 自定义异常
  • 尽量捕获原始异常
  • 不要打印堆栈后再抛出异常
  • 不要用异常处理机制代替判断
  • 不要过早捕获异常

6)注解

  • 注解是什么?
  • 注解的生命周期
  • 注解装饰的目标
  • 自定义注解
  • 使用注解

7)IO 流

  • 字符流、字节流
  • 输入流、输出流
  • 同步、异步
  • 阻塞、非阻塞
  • BIO、NIO 和 AIO
  • NIO 2.0

8)序列化

  • 什么是序列化和反序列化
  • Java 如何实现序列化和反序列化
  • Serializbale 和 Externalizable
  • serialVersionUID

9)泛型

10)单元测试

  • Junit
  • TestNG

11)编码方式

  • ASCII
  • Unicode
  • UTF-8
  • GBK、GB2312
  • 如何解决乱码问题

12)并发编程

  • 什么是并发
  • 什么是并行
  • 什么是线程
  • 什么是进程
  • 线程的状态
  • 线程的优先级
  • 创建线程
  • 创建线程池
  • 什么是线程安全
  • 多级缓存和一致性问题
  • CPU 时间片和原子性问题
  • 指令重排和有序性问题
  • 线程安全和内存模型
  • happens-before
  • 可重入锁
  • 阻塞锁
  • 乐观锁
  • 悲观锁
  • 分布式锁
  • CAS
  • ABA
  • 偏向锁
  • 轻量级锁
  • 重量级锁
  • 自旋锁
  • 什么是死锁
  • 如果避免死锁
  • synchronized
  • volatile
  • ThreadLocal
  • Executors
  • CountDownLatch
  • Thread
  • Runnable
  • Callable
  • ReentrantLock
  • ReentrantReadWriteLock
  • Atomic 相关类

13)Java 8 新特性

14)源码阅读

  • String
  • Integer
  • ArrayList
  • LinkedList
  • CopyOnWriteArrayList
  • HashMap
  • TreeMap
  • LinkedHashMap
  • ConcurrentHashMap
  • CopyOnWriteArrayList

04、第四个阶段,Java 进阶升级

1)JVM

  • Java 内存结构
  • 垃圾回收
  • JVM 参数调优
  • Java 对象模型
  • HotSpot
  • 类加载机制
  • 编译和反编译
  • 反编译工具
  • JIT
  • 虚拟机性能监控和故障处理工具(jps、jstack、jmap、jstat、jconsole、javap)

2)性能优化

  • 使用单例
  • 使用线程池
  • 减少上下文切换
  • 减小锁粒度
  • 数据压缩
  • Stream 并行流
  • GC 调优
  • JVM 内存分配调优
  • btrace

3)设计模式

  • 设计模式的六大原则
  • 创建型设计模式(单例、抽象工厂、建造者、工厂、原型)
  • 结构型设计模式(适配器、桥接、装饰、组合、外观、享元、代理)
  • 行为型设计模式(模板方法、命令、迭代器、观察者、中介者、备忘录、解释器、状态、策略、责任链、访问者)
  • 单例的七种写法

4)数据结构和算法

  • 简单的数据结构(栈、队列、链表、数组、哈希表)
  • 树(二叉树、字典树、平衡树、排序树、B 树、B+ 树、R 树、红黑树、多路树)
  • 图(拓扑、有向图、无向图)
  • 稳定的排序算法(冒泡排序、插入排序、鸡尾酒排序、桶排序、计数排序、归并排序、原地归并排序、二叉排序树排序、鸽巢排序、基数排序、侏儒排序、图书馆排序、块排序)
  • 不稳定的排序算法(选择排序、希尔排序、梳排序、堆排序、平滑排序、快速排序、内省排序、耐心排序、Clover 排序)
  • 时间复杂度
  • 空间复杂度
  • 贪心算法
  • KMP 算法

5)操作系统

  • Linux 常用命令(find、top、tar、move、grep、tail、netstat、curl、wget、ping、ssh)
  • 服务器性能指标(qps、CPU 利用率)
  • 进程同步
  • 分段和分页
  • 虚拟内存和主存

6)网络安全

  • CSRF
  • XSS
  • SQL 注入
  • 加密和解密(对称加密、非对称加密)
  • MD5、SHA1、DES、RSA
  • DDOS 攻击
  • HTTP 和 HTTPS
  • SSL
  • TLS
  • TCP 和 UDP
  • Cookie、Session
  • CDN
  • DNS

7)数据库

  • MySql
  • 索引
  • 存储过程
  • 分库分表
  • binlog
  • 读写分离
  • 数据库缓存(RedisMongoDB
  • 数据库中间件(MyCat)
  • 数据库连接池(Durid)

8)大数据

  • 搜索(Elasticsearch 、Solr)
  • 流式计算(Storm、Spark、Flink)
  • Hadoop

9)服务器

  • Tomcat
  • jetty
  • Nginx

10)框架

  • Spring
  • MyBatis
  • Spring MVC
  • Spring Boot
  • Spring Security
  • Spring Cloud
  • Netty
  • Dubbo

11)消息队列

12)容器

  • Docker
  • K8s
需要 Java 书单的话,我在 GitHub 上发现了一个宝藏项目,光看了一下目录,就有点吸引我。需要的小伙伴可以按需自取,地址如下所示:

https://github.com/itwanger/J...

05、第五个阶段,活着最重要

技术是没有终点的,也是学不完的,最重要的是活着、不秃。

零基础入门的时候看书还是看视频,我觉得成年人,何必做选择题呢,两个都要。喜欢看书就看书,喜欢看视频就看视频。

最重要的是在自学的过程中,一定不要眼高手低,要实战,把学到的技术投入到项目当中,解决问题,之后进一步锤炼自己的技术。

开源的项目我推荐 GitHub 上的 mall 和 vhr,前者是电商系统,后者是微人事,都用的最前言的技术,并且文档很全面,不怕晕头转向。

自学最怕的就是缺乏自驱力,一定要自律,杜绝“三天打鱼两天晒网”,到最后白忙活一场。

高度自律的同时,要保持耐心,不抛弃不放弃,切勿自怨自艾,每天给自己一点点鼓励,学习的劲头就会很足,不容易犯困。

技术学到手后,找工作的时候一定要好好准备一份简历,不要无头苍蝇一样去海投简历,容易“竹篮打水一场空”。可以参考下面的链接,好好的准备一下简历,毕竟是找工作的敲门砖。

入职阿里后,才知道原来简历这么写

拿到面试邀请后,在面试的过程中一定要大大方方,尽力把自己学到的知识舒适地表达出来,不要因为是自学就不够自信,给面试官一个好的印象,面试成功的几率就会大很多,加油吧,骚年!

查看原文

赞 12 收藏 8 评论 0

沉默王二 发布了文章 · 9月28日

真服了,ArrayList和LinkedList差别竟然这么大

ArrayList 和 LinkedList 有什么区别,是面试官非常喜欢问的一个问题。可能大部分小伙伴和我一样,能回答出“ArrayList 是基于数组实现的,LinkedList 是基于双向链表实现的。”

关于这一点,我之前的文章里也提到过了。但说实话,这样苍白的回答并不能令面试官感到满意,他还想知道的更多。

那假如小伙伴们继续做出下面这样的回答:

“ArrayList 在新增和删除元素时,因为涉及到数组复制,所以效率比 LinkedList 低,而在遍历的时候,ArrayList 的效率要高于 LinkedList。”

面试官会感到满意吗?我只能说,如果面试官比较仁慈的话,他可能会让我们回答下一个问题;否则的话,他会让我们回家等通知,这一等,可能意味着杳无音讯了。

为什么会这样呢?为什么为什么?回答的不对吗?

暴躁的小伙伴请喝口奶茶冷静一下。冷静下来后,请随我来,让我们一起肩并肩、手拉手地深入地研究一下 ArrayList 和 LinkedList 的数据结构、实现原理以及源码,可能神秘的面纱就揭开了。

01、ArrayList 是如何实现的?

ArrayList 实现了 List 接口,继承了 AbstractList 抽象类,底层是基于数组实现的,并且实现了动态扩容。

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
    private static final int DEFAULT_CAPACITY = 10;
    transient Object[] elementData;
    private int size;
}

ArrayList 还实现了 RandomAccess 接口,这是一个标记接口:

public interface RandomAccess {
}

内部是空的,标记“实现了这个接口的类支持快速(通常是固定时间)随机访问”。快速随机访问是什么意思呢?就是说不需要遍历,就可以通过下标(索引)直接访问到内存地址。

public E get(int index) {
    Objects.checkIndex(index, size);
    return elementData(index);
}
E elementData(int index) {
    return (E) elementData[index];
}

ArrayList 还实现了 Cloneable 接口,这表明 ArrayList 是支持拷贝的。ArrayList 内部的确也重写了 Object 类的 clone() 方法。

public Object clone() {
    try {
        ArrayList<?> v = (ArrayList<?>) super.clone();
        v.elementData = Arrays.copyOf(elementData, size);
        v.modCount = 0;
        return v;
    } catch (CloneNotSupportedException e) {
        // this shouldn't happen, since we are Cloneable
        throw new InternalError(e);
    }
}

ArrayList 还实现了 Serializable 接口,同样是一个标记接口:

public interface Serializable {
}

内部也是空的,标记“实现了这个接口的类支持序列化”。序列化是什么意思呢?Java 的序列化是指,将对象转换成以字节序列的形式来表示,这些字节序中包含了对象的字段和方法。序列化后的对象可以被写到数据库、写到文件,也可用于网络传输。

眼睛雪亮的小伙伴可能会注意到,ArrayList 中的关键字段 elementData 使用了 transient 关键字修饰,这个关键字的作用是,让它修饰的字段不被序列化。

这不前后矛盾吗?一个类既然实现了 Serilizable 接口,肯定是想要被序列化的,对吧?那为什么保存关键数据的 elementData 又不想被序列化呢?

这还得从 “ArrayList 是基于数组实现的”开始说起。大家都知道,数组是定长的,就是说,数组一旦声明了,长度(容量)就是固定的,不能像某些东西一样伸缩自如。这就很麻烦,数组一旦装满了,就不能添加新的元素进来了。

ArrayList 不想像数组这样活着,它想能屈能伸,所以它实现了动态扩容。一旦在添加元素的时候,发现容量用满了 s == elementData.length,就按照原来数组的 1.5 倍(oldCapacity >> 1)进行扩容。扩容之后,再将原有的数组复制到新分配的内存地址上 Arrays.copyOf(elementData, newCapacity)

private void add(E e, Object[] elementData, int s) {
    if (s == elementData.length)
        elementData = grow();
    elementData[s] = e;
    size = s + 1;
}

private Object[] grow() {
    return grow(size + 1);
}

private Object[] grow(int minCapacity) {
    int oldCapacity = elementData.length;
    if (oldCapacity > 0 || elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        int newCapacity = ArraysSupport.newLength(oldCapacity,
                minCapacity - oldCapacity, /* minimum growth */
                oldCapacity >> 1           /* preferred growth */);
        return elementData = Arrays.copyOf(elementData, newCapacity);
    } else {
        return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)];
    }
}

动态扩容意味着什么?大家伙想一下。嗯,还是我来告诉大家答案吧,有点迫不及待。

意味着数组的实际大小可能永远无法被填满的,总有多余出来空置的内存空间。

比如说,默认的数组大小是 10,当添加第 11 个元素的时候,数组的长度扩容了 1.5 倍,也就是 15,意味着还有 4 个内存空间是闲置的,对吧?

序列化的时候,如果把整个数组都序列化的话,是不是就多序列化了 4 个内存空间。当存储的元素数量非常非常多的时候,闲置的空间就非常非常大,序列化耗费的时间就会非常非常多。

于是,ArrayList 做了一个愉快而又聪明的决定,内部提供了两个私有方法 writeObject 和 readObject 来完成序列化和反序列化。

private void writeObject(java.io.ObjectOutputStream s)
        throws java.io.IOException {
    // Write out element count, and any hidden stuff
    int expectedModCount = modCount;
    s.defaultWriteObject();

    // Write out size as capacity for behavioral compatibility with clone()
    s.writeInt(size);

    // Write out all elements in the proper order.
    for (int i=0; i<size; i++) {
        s.writeObject(elementData[i]);
    }

    if (modCount != expectedModCount) {
        throw new ConcurrentModificationException();
    }
}

从 writeObject 方法的源码中可以看得出,它使用了 ArrayList 的实际大小 size 而不是数组的长度(elementData.length)来作为元素的上限进行序列化。

此处应该有掌声啊!不是为我,为 Java 源码的作者们,他们真的是太厉害了,可以用两个词来形容他们——殚精竭虑、精益求精。

02、LinkedList 是如何实现的?

LinkedList 是一个继承自 AbstractSequentialList 的双向链表,因此它也可以被当作堆栈、队列或双端队列进行操作。

public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
    transient int size = 0;
    transient Node<E> first;
    transient Node<E> last;
}

LinkedList 内部定义了一个 Node 节点,它包含 3 个部分:元素内容 item,前引用 prev 和后引用 next。代码如下所示:

private static class Node<E> {
    E item;
    LinkedList.Node<E> next;
    LinkedList.Node<E> prev;

    Node(LinkedList.Node<E> prev, E element, LinkedList.Node<E> next) {
        this.item = element;
        this.next = next;
        this.prev = prev;
    }
}

LinkedList 还实现了 Cloneable 接口,这表明 LinkedList 是支持拷贝的。

LinkedList 还实现了 Serializable 接口,这表明 LinkedList 是支持序列化的。眼睛雪亮的小伙伴可能又注意到了,LinkedList 中的关键字段 size、first、last 都使用了 transient 关键字修饰,这不又矛盾了吗?到底是想序列化还是不想序列化?

答案是 LinkedList 想按照自己的方式序列化,来看它自己实现的 writeObject() 方法:

private void writeObject(java.io.ObjectOutputStream s)
        throws java.io.IOException {
    // Write out any hidden serialization magic
    s.defaultWriteObject();

    // Write out size
    s.writeInt(size);

    // Write out all elements in the proper order.
    for (LinkedList.Node<E> x = first; x != null; x = x.next)
        s.writeObject(x.item);
}

发现没?LinkedList 在序列化的时候只保留了元素的内容 item,并没有保留元素的前后引用。这样就节省了不少内存空间,对吧?

那有些小伙伴可能就疑惑了,只保留元素内容,不保留前后引用,那反序列化的时候怎么办?

private void readObject(java.io.ObjectInputStream s)
        throws java.io.IOException, ClassNotFoundException {
    // Read in any hidden serialization magic
    s.defaultReadObject();

    // Read in size
    int size = s.readInt();

    // Read in all elements in the proper order.
    for (int i = 0; i < size; i++)
        linkLast((E)s.readObject());
}

void linkLast(E e) {
    final LinkedList.Node<E> l = last;
    final LinkedList.Node<E> newNode = new LinkedList.Node<>(l, e, null);
    last = newNode;
    if (l == null)
        first = newNode;
    else
        l.next = newNode;
    size++;
    modCount++;
}

注意 for 循环中的 linkLast() 方法,它可以把链表重新链接起来,这样就恢复了链表序列化之前的顺序。很妙,对吧?

和 ArrayList 相比,LinkedList 没有实现 RandomAccess 接口,这是因为 LinkedList 存储数据的内存地址是不连续的,所以不支持随机访问。

03、ArrayList 和 LinkedList 新增元素时究竟谁快?

前面我们已经从多个维度了解了 ArrayList 和 LinkedList 的实现原理和各自的特点。那接下来,我们就来聊聊 ArrayList 和 LinkedList 在新增元素时究竟谁快?

1)ArrayList

ArrayList 新增元素有两种情况,一种是直接将元素添加到数组末尾,一种是将元素插入到指定位置。

添加到数组末尾的源码:

public boolean add(E e) {
    modCount++;
    add(e, elementData, size);
    return true;
}

private void add(E e, Object[] elementData, int s) {
    if (s == elementData.length)
        elementData = grow();
    elementData[s] = e;
    size = s + 1;
}

很简单,先判断是否需要扩容,然后直接通过索引将元素添加到末尾。

插入到指定位置的源码:

public void add(int index, E element) {
    rangeCheckForAdd(index);
    modCount++;
    final int s;
    Object[] elementData;
    if ((s = size) == (elementData = this.elementData).length)
        elementData = grow();
    System.arraycopy(elementData, index,
            elementData, index + 1,
            s - index);
    elementData[index] = element;
    size = s + 1;
}

先检查插入的位置是否在合理的范围之内,然后判断是否需要扩容,再把该位置以后的元素复制到新添加元素的位置之后,最后通过索引将元素添加到指定的位置。这种情况是非常伤的,性能会比较差。

2)LinkedList

LinkedList 新增元素也有两种情况,一种是直接将元素添加到队尾,一种是将元素插入到指定位置。

添加到队尾的源码:

public boolean add(E e) {
    linkLast(e);
    return true;
}
void linkLast(E e) {
    final LinkedList.Node<E> l = last;
    final LinkedList.Node<E> newNode = new LinkedList.Node<>(l, e, null);
    last = newNode;
    if (l == null)
        first = newNode;
    else
        l.next = newNode;
    size++;
    modCount++;
}

先将队尾的节点 last 存放到临时变量 l 中(不是说不建议使用 I 作为变量名吗?Java 的作者们明知故犯啊),然后生成新的 Node 节点,并赋给 last,如果 l 为 null,说明是第一次添加,所以 first 为新的节点;否则将新的节点赋给之前 last 的 next。

插入到指定位置的源码:

public void add(int index, E element) {
    checkPositionIndex(index);

    if (index == size)
        linkLast(element);
    else
        linkBefore(element, node(index));
}
LinkedList.Node<E> node(int index) {
    // assert isElementIndex(index);

    if (index < (size >> 1)) {
        LinkedList.Node<E> x = first;
        for (int i = 0; i < index; i++)
            x = x.next;
        return x;
    } else {
        LinkedList.Node<E> x = last;
        for (int i = size - 1; i > index; i--)
            x = x.prev;
        return x;
    }
}
void linkBefore(E e, LinkedList.Node<E> succ) {
    // assert succ != null;
    final LinkedList.Node<E> pred = succ.prev;
    final LinkedList.Node<E> newNode = new LinkedList.Node<>(pred, e, succ);
    succ.prev = newNode;
    if (pred == null)
        first = newNode;
    else
        pred.next = newNode;
    size++;
    modCount++;
}

先检查插入的位置是否在合理的范围之内,然后判断插入的位置是否是队尾,如果是,添加到队尾;否则执行 linkBefore() 方法。

在执行 linkBefore() 方法之前,会调用 node() 方法查找指定位置上的元素,这一步是需要遍历 LinkedList 的。如果插入的位置靠前前半段,就从队头开始往后找;否则从队尾往前找。也就是说,如果插入的位置越靠近 LinkedList 的中间位置,遍历所花费的时间就越多。

找到指定位置上的元素(succ)之后,就开始执行 linkBefore() 方法了,先将 succ 的前一个节点(prev)存放到临时变量 pred 中,然后生成新的 Node 节点(newNode),并将 succ 的前一个节点变更为 newNode,如果 pred 为 null,说明插入的是队头,所以 first 为新节点;否则将 pred 的后一个节点变更为 newNode。

经过源码分析以后,小伙伴们是不是在想:“好像 ArrayList 在新增元素的时候效率并不一定比 LinkedList 低啊!”

当两者的起始长度是一样的情况下:

  • 如果是从集合的头部新增元素,ArrayList 花费的时间应该比 LinkedList 多,因为需要对头部以后的元素进行复制。
public class ArrayListTest {
    public static void addFromHeaderTest(int num) {
        ArrayList<String> list = new ArrayList<String>(num);
        int i = 0;

        long timeStart = System.currentTimeMillis();

        while (i < num) {
            list.add(0, i + "沉默王二");
            i++;
        }
        long timeEnd = System.currentTimeMillis();

        System.out.println("ArrayList从集合头部位置新增元素花费的时间" + (timeEnd - timeStart));
    }
}

/**
 * @author 微信搜「沉默王二」,回复关键字 PDF
 */
public class LinkedListTest {
    public static void addFromHeaderTest(int num) {
        LinkedList<String> list = new LinkedList<String>();
        int i = 0;
        long timeStart = System.currentTimeMillis();
        while (i < num) {
            list.addFirst(i + "沉默王二");
            i++;
        }
        long timeEnd = System.currentTimeMillis();

        System.out.println("LinkedList从集合头部位置新增元素花费的时间" + (timeEnd - timeStart));
    }
}

num 为 10000,代码实测后的时间如下所示:

ArrayList从集合头部位置新增元素花费的时间595
LinkedList从集合头部位置新增元素花费的时间15

ArrayList 花费的时间比 LinkedList 要多很多。

  • 如果是从集合的中间位置新增元素,ArrayList 花费的时间搞不好要比 LinkedList 少,因为 LinkedList 需要遍历。
public class ArrayListTest {
    public static void addFromMidTest(int num) {
        ArrayList<String> list = new ArrayList<String>(num);
        int i = 0;

        long timeStart = System.currentTimeMillis();
        while (i < num) {
            int temp = list.size();
            list.add(temp / 2 + "沉默王二");
            i++;
        }
        long timeEnd = System.currentTimeMillis();

        System.out.println("ArrayList从集合中间位置新增元素花费的时间" + (timeEnd - timeStart));
    }
}

public class LinkedListTest {
    public static void addFromMidTest(int num) {
        LinkedList<String> list = new LinkedList<String>();
        int i = 0;
        long timeStart = System.currentTimeMillis();
        while (i < num) {
            int temp = list.size();
            list.add(temp / 2, i + "沉默王二");
            i++;
        }
        long timeEnd = System.currentTimeMillis();

        System.out.println("LinkedList从集合中间位置新增元素花费的时间" + (timeEnd - timeStart));
    }
}

num 为 10000,代码实测后的时间如下所示:

ArrayList从集合中间位置新增元素花费的时间1
LinkedList从集合中间位置新增元素花费的时间101

ArrayList 花费的时间比 LinkedList 要少很多很多。

  • 如果是从集合的尾部新增元素,ArrayList 花费的时间应该比 LinkedList 少,因为数组是一段连续的内存空间,也不需要复制数组;而链表需要创建新的对象,前后引用也要重新排列。
public class ArrayListTest {
    public static void addFromTailTest(int num) {
        ArrayList<String> list = new ArrayList<String>(num);
        int i = 0;

        long timeStart = System.currentTimeMillis();

        while (i < num) {
            list.add(i + "沉默王二");
            i++;
        }

        long timeEnd = System.currentTimeMillis();

        System.out.println("ArrayList从集合尾部位置新增元素花费的时间" + (timeEnd - timeStart));
    }
}

public class LinkedListTest {
    public static void addFromTailTest(int num) {
        LinkedList<String> list = new LinkedList<String>();
        int i = 0;
        long timeStart = System.currentTimeMillis();
        while (i < num) {
            list.add(i + "沉默王二");
            i++;
        }
        long timeEnd = System.currentTimeMillis();

        System.out.println("LinkedList从集合尾部位置新增元素花费的时间" + (timeEnd - timeStart));
    }
}

num 为 10000,代码实测后的时间如下所示:

ArrayList从集合尾部位置新增元素花费的时间69
LinkedList从集合尾部位置新增元素花费的时间193

ArrayList 花费的时间比 LinkedList 要少一些。

这样的结论和预期的是不是不太相符?ArrayList 在添加元素的时候如果不涉及到扩容,性能在两种情况下(中间位置新增元素、尾部新增元素)比 LinkedList 好很多,只有头部新增元素的时候比 LinkedList 差,因为数组复制的原因。

当然了,如果涉及到数组扩容的话,ArrayList 的性能就没那么可观了,因为扩容的时候也要复制数组。

04、ArrayList 和 LinkedList 删除元素时究竟谁快?

1)ArrayList

ArrayList 删除元素的时候,有两种方式,一种是直接删除元素(remove(Object)),需要直先遍历数组,找到元素对应的索引;一种是按照索引删除元素(remove(int))。

public boolean remove(Object o) {
    final Object[] es = elementData;
    final int size = this.size;
    int i = 0;
    found: {
        if (o == null) {
            for (; i < size; i++)
                if (es[i] == null)
                    break found;
        } else {
            for (; i < size; i++)
                if (o.equals(es[i]))
                    break found;
        }
        return false;
    }
    fastRemove(es, i);
    return true;
}
public E remove(int index) {
    Objects.checkIndex(index, size);
    final Object[] es = elementData;

    @SuppressWarnings("unchecked") E oldValue = (E) es[index];
    fastRemove(es, index);

    return oldValue;
}

但从本质上讲,都是一样的,因为它们最后调用的都是 fastRemove(Object, int) 方法。

private void fastRemove(Object[] es, int i) {
    modCount++;
    final int newSize;
    if ((newSize = size - 1) > i)
        System.arraycopy(es, i + 1, es, i, newSize - i);
    es[size = newSize] = null;
}

从源码可以看得出,只要删除的不是最后一个元素,都需要数组重组。删除的元素位置越靠前,代价就越大。

2)LinkedList

LinkedList 删除元素的时候,有四种常用的方式:

  • remove(int),删除指定位置上的元素
public E remove(int index) {
    checkElementIndex(index);
    return unlink(node(index));
}

先检查索引,再调用 node(int) 方法( 前后半段遍历,和新增元素操作一样)找到节点 Node,然后调用 unlink(Node) 解除节点的前后引用,同时更新前节点的后引用和后节点的前引用:

    E unlink(Node<E> x) {
        // assert x != null;
        final E element = x.item;
        final Node<E> next = x.next;
        final Node<E> prev = x.prev;

        if (prev == null) {
            first = next;
        } else {
            prev.next = next;
            x.prev = null;
        }

        if (next == null) {
            last = prev;
        } else {
            next.prev = prev;
            x.next = null;
        }

        x.item = null;
        size--;
        modCount++;
        return element;
    }
  • remove(Object),直接删除元素
public boolean remove(Object o) {
    if (o == null) {
        for (LinkedList.Node<E> x = first; x != null; x = x.next) {
            if (x.item == null) {
                unlink(x);
                return true;
            }
        }
    } else {
        for (LinkedList.Node<E> x = first; x != null; x = x.next) {
            if (o.equals(x.item)) {
                unlink(x);
                return true;
            }
        }
    }
    return false;
}

也是先前后半段遍历,找到要删除的元素后调用 unlink(Node)

  • removeFirst(),删除第一个节点
public E removeFirst() {
    final LinkedList.Node<E> f = first;
    if (f == null)
        throw new NoSuchElementException();
    return unlinkFirst(f);
}
private E unlinkFirst(LinkedList.Node<E> f) {
    // assert f == first && f != null;
    final E element = f.item;
    final LinkedList.Node<E> next = f.next;
    f.item = null;
    f.next = null; // help GC
    first = next;
    if (next == null)
        last = null;
    else
        next.prev = null;
    size--;
    modCount++;
    return element;
}

删除第一个节点就不需要遍历了,只需要把第二个节点更新为第一个节点即可。

  • removeLast(),删除最后一个节点

删除最后一个节点和删除第一个节点类似,只需要把倒数第二个节点更新为最后一个节点即可。

可以看得出,LinkedList 在删除比较靠前和比较靠后的元素时,非常高效,但如果删除的是中间位置的元素,效率就比较低了。

这里就不再做代码测试了,感兴趣的小伙伴可以自己试试,结果和新增元素保持一致:

  • 从集合头部删除元素时,ArrayList 花费的时间比 LinkedList 多很多;
  • 从集合中间位置删除元素时,ArrayList 花费的时间比 LinkedList 少很多;
  • 从集合尾部删除元素时,ArrayList 花费的时间比 LinkedList 少一点。

我本地的统计结果如下所示,小伙伴们可以作为参考:

ArrayList从集合头部位置删除元素花费的时间380
LinkedList从集合头部位置删除元素花费的时间4
ArrayList从集合中间位置删除元素花费的时间381
LinkedList从集合中间位置删除元素花费的时间5922
ArrayList从集合尾部位置删除元素花费的时间8
LinkedList从集合尾部位置删除元素花费的时间12

05、ArrayList 和 LinkedList 遍历元素时究竟谁快?

1)ArrayList

遍历 ArrayList 找到某个元素的话,通常有两种形式:

  • get(int),根据索引找元素
public E get(int index) {
    Objects.checkIndex(index, size);
    return elementData(index);
}

由于 ArrayList 是由数组实现的,所以根据索引找元素非常的快,一步到位。

  • indexOf(Object),根据元素找索引
public int indexOf(Object o) {
    return indexOfRange(o, 0, size);
}

int indexOfRange(Object o, int start, int end) {
    Object[] es = elementData;
    if (o == null) {
        for (int i = start; i < end; i++) {
            if (es[i] == null) {
                return i;
            }
        }
    } else {
        for (int i = start; i < end; i++) {
            if (o.equals(es[i])) {
                return i;
            }
        }
    }
    return -1;
}

根据元素找索引的话,就需要遍历整个数组了,从头到尾依次找。

2)LinkedList

遍历 LinkedList 找到某个元素的话,通常也有两种形式:

  • get(int),找指定位置上的元素
public E get(int index) {
    checkElementIndex(index);
    return node(index).item;
}

既然需要调用 node(int) 方法,就意味着需要前后半段遍历了。

  • indexOf(Object),找元素所在的位置
public int indexOf(Object o) {
    int index = 0;
    if (o == null) {
        for (LinkedList.Node<E> x = first; x != null; x = x.next) {
            if (x.item == null)
                return index;
            index++;
        }
    } else {
        for (LinkedList.Node<E> x = first; x != null; x = x.next) {
            if (o.equals(x.item))
                return index;
            index++;
        }
    }
    return -1;
}

需要遍历整个链表,和 ArrayList 的 indexOf() 类似。

那在我们对集合遍历的时候,通常有两种做法,一种是使用 for 循环,一种是使用迭代器(Iterator)。

如果使用的是 for 循环,可想而知 LinkedList 在 get 的时候性能会非常差,因为每一次外层的 for 循环,都要执行一次 node(int) 方法进行前后半段的遍历。

LinkedList.Node<E> node(int index) {
    // assert isElementIndex(index);

    if (index < (size >> 1)) {
        LinkedList.Node<E> x = first;
        for (int i = 0; i < index; i++)
            x = x.next;
        return x;
    } else {
        LinkedList.Node<E> x = last;
        for (int i = size - 1; i > index; i--)
            x = x.prev;
        return x;
    }
}

那如果使用的是迭代器呢?

LinkedList<String> list = new LinkedList<String>();
for (Iterator<String> it = list.iterator(); it.hasNext();) {
    it.next();
}

迭代器只会调用一次 node(int) 方法,在执行 list.iterator() 的时候:先调用 AbstractSequentialList 类的 iterator() 方法,再调用 AbstractList 类的 listIterator() 方法,再调用 LinkedList 类的 listIterator(int) 方法,如下图所示。

最后返回的是 LinkedList 类的内部私有类 ListItr 对象:

public ListIterator<E> listIterator(int index) {
    checkPositionIndex(index);
    return new LinkedList.ListItr(index);
}

private class ListItr implements ListIterator<E> {
    private LinkedList.Node<E> lastReturned;
    private LinkedList.Node<E> next;
    private int nextIndex;
    private int expectedModCount = modCount;

    ListItr(int index) {
        // assert isPositionIndex(index);
        next = (index == size) ? null : node(index);
        nextIndex = index;
    }

    public boolean hasNext() {
        return nextIndex < size;
    }

    public E next() {
        checkForComodification();
        if (!hasNext())
            throw new NoSuchElementException();

        lastReturned = next;
        next = next.next;
        nextIndex++;
        return lastReturned.item;
    }
}

执行 ListItr 的构造方法时调用了一次 node(int) 方法,返回第一个节点。在此之后,迭代器就执行 hasNext() 判断有没有下一个,执行 next() 方法下一个节点。

由此,可以得出这样的结论:遍历 LinkedList 的时候,千万不要使用 for 循环,要使用迭代器。

也就是说,for 循环遍历的时候,ArrayList 花费的时间远小于 LinkedList;迭代器遍历的时候,两者性能差不多。

06、总结

花了两天时间,终于肝完了!相信看完这篇文章后,再有面试官问你 ArrayList 和 LinkedList 有什么区别的话,你一定会胸有成竹地和他扯上半小时。

另外,我把自己看过的学习视频按照顺序分了类,共 500G,目录如下,还有 2020 年最新面试题,现在免费送给大家

链接:https://pan.baidu.com/s/1j2uB7-TF3t5BAzVXBgV7dA 密码:cg1q

我是沉默王二,一枚沉默但有趣的程序员,感谢各位同学的:点赞、收藏和评论,我们下篇见!

查看原文

赞 13 收藏 10 评论 1

沉默王二 发布了文章 · 9月24日

软件开发的 5 条核心原则,让工作事半功倍

作为一名程序员,小伙伴们有没有想过这个简单的问题,“软件是什么?”可以闭上眼睛让自己想一会,如果觉得有点抽象不太好回答的话,来看看我的答案。

软件 = 程序 + 数据 + 文档 + (服务)
程序 = 数据结构 + 算法

看完这两个直观的公式,是不是有一种恍然大悟的感觉,“哦,原来这样啊。”

再来看四条对“软件”的定义,虽然比较枯燥,但概念是到位的:

  • 软件是能够完成预定功能,达到预期性能的,可以执行的计算机指令;
  • 软件是能够让程序处理适当信息的数据结构;
  • 软件是描述程序操作和使用的文档;
  • 软件是一种逻辑实体,具备知识性的产品集合,是对物理世界的一种抽象,同时又是一种人脑智力的成果。

在很多自以为是的甲方眼里,软件是廉价的,可以随意复制的,因此他们经常提出一些苛刻的要求,其中有一些让软件开发者感到哭笑不得:“这个需求简单的嘞,你去网上随便找个现成的,改一改就好了呀,花不了多长时间的,一个月可以搞定吧?”每次听到类似的话,我的心里就有一万只草泥马奔腾而过。

软件开发并不是一件轻而易举的事情,需要经历下面这些基本过程:

1)软件计划,确定产品定位和目标用户。这一步是需要甲方去规划和调研的。

2)软件需求分析:根据甲方需求,分析出甲方需要的产品功能。这一步是需要项目负责人(或者产品经理)去和甲方沟通的。

3)根据需求进行设计:包括概要设计和详细设计。这一步是需要项目负责人(或产品经理)做的,并且要正确地传达给开发人员。

4)编码并运行。这一步是需要开发人员去做的。

5)测试:确认甲方需求,对设计和结果进行验证。开发人员要进行单元测试,集成测试,如果有专业的测试团队的话,就需要站在甲方和用户的角度去测试整体产品是否符合要求并达到性能要求。

6)维护:保证软件能够在正式环境下运行,并且对一些缺陷(bug)进行修正,或者对功能进行完善,或者对性能进行改进,不断迭代软件版本。

瞧,软件开发的过程并没有甲方想象中那么简单,如果有小伙伴遇到不讲理的甲方,就把这篇文章扔给他好好看看。

既然软件开发的过程是有难度的,是需要付出时间和精力的,那就有必要遵循一些原则,否则开发成本就会变得很昂贵,开发周期就会拖延很长时间。

原则一: Don't Repeat Yourself

直译叫做“不要重复你自己”,还有另外一个耳熟能详的版本,“不要重复造轮子”。

在你一开始进入软件开发这个领域后,就一定要注意,把你自己写过的一些解决方案汇总到一起,定期梳理一遍,写点文档,不断重构,使它们成为一把把瑞士军刀。如果可以的话,把它们开源出来,服务更多的开发者。

有了自己的工具库后,当你下次遇到类似的需求时,就可以直接拿出来用,省去不少时间。

除此之外,你还应该善于利用那些业界已经开源出来的成熟的技术方案,比如下面这些。

GitHub 和码云是两个充满宝藏的地方,如果你觉得自己的能力还不到自己造轮子的份上,那就一定要多上上这两个网站,里面有很多成熟的解决方案供你免费使用。

比如说,你要一套商城系统,那么 marcozheng 的 mall 就可以直接拿来作为原型。比如说,你要一套人事管理系统,那么江南一点雨的 vhr 就可以直接拿来作为原型。(虽然推荐了很多次,但好朋友的,多推荐一次不嫌多。)

原则二: Keep it simple stupid

著名的 KISS 原则,即“保持简单、保持愚蠢”,和史蒂夫·乔布斯的名言“stay hungry, stay foolish”有着异曲同工之妙。

从苹果产品的设计上也可以体现出来这个原则,起初的手机,比如说诺基亚智能机,带很多实体键,但苹果只有一个 home 键,其他全部虚拟键代替,彻底革了诺基亚的命。

在我们设计软件的过程中,千万不要想得太复杂,越简单越好,等成型了以后再丰富效果,否则开发成本会变得很昂贵,软件就可以腹死胎中。

原则三: You Ain't Gonna Need It

英文直译为“你不需要它”,该规则要求程序员在必要之前不应该添加功能。极限编程的联合创始人罗恩·杰弗里斯(Ron Jeffries)曾经说过:“总是在实际需要时才实现事物,而不是在预见到需要它们时才实现。”

项目负责人(产品经理)更应该坚持这条原则,千万不要过度拆解用户的需求,在产品设计的过程追加过多自己认为应该追加的功能,因为在一个软件使用中,往往 80% 的请求都花费在 20% 的功能上。

很多次要的功能可能需要,因为它们的存在而使软件锦上添花,但没有它们,软件的商业价值依然是存在的。功能越少,开发周期就会越短,这样就更有可能打败竞品。

原则四: Done is better than perfect

Done is better than perfect because perfect is never done。

很简单的一句英文,能理解吧?

不要总想着把所有的功能做完善,做完美后再上线,应该在产品具有一定的雏形后就立即上线试错,根据用户的反馈,根据市场的需求再去考量是否追加一些其他的功能或者优化。

“人无完人,金无足赤”,应该允许一些瑕疵存在,刻意追求完美并不见得是一件好事。乔布斯想要一整块屏幕,但技术达不到的时候,他也是会留一个 home 键的。

我们程序员在开发软件的时候,也应该遵循这条原则,先把功能做出来再说,至于效果,用户的体验,应该往后放,不要总想着尽善尽美,尽善尽美意味着永远也完不成——没有最好,只有更好。

原则五: Choose the most suitable things

选择最适合的,不要盲目追求时髦。技术日新月异,应接不暇,如果在开发软件的时候,一味追求最前沿的技术,可能就会让产品变成小白鼠。

就好像我们谈一场恋爱,不要一味去追求高不可攀的,往往那些在我们身边的,肯陪伴我们的才是最好的。

技术选型的时候,适合就好。如果产品的目标用户只有一千人不到,就没必要搞分布式,搞大数据,否则就有点“蛇吞象”的意味;等真到了需要搞分布式,搞大数据的时候再升级完全来得及。

肝了三天三夜,《程序员不可或缺的软实力》第一版强势来袭,纯手敲,足足 20 万字精华文章,贯穿了我十余年的编程生涯,涉及到了生活和工作中的方方面面,如果你是迷茫的在校大学生,或者刚入职的新人,相信我的个人经历,可以给你带去一些思考,从而树立起正确的人生观和价值观。

那这份 PDF 该怎么获取呢?

链接:https://pan.baidu.com/s/1o6MY84my0OD0DHnAmZT6rA 密码:tx5e

真心希望这份 PDF 能够对大家起到实质性的帮助,我也会在后面不断完善这本电子书,敬请期待。

当然,也日常求个赞!

最后,希望小伙伴们在软件开发的过程中,能够去遵循这 5 条原则,毕竟每天工作的时候可以多摸鱼 4 个小时(手动狗头)。

查看原文

赞 8 收藏 3 评论 0

沉默王二 发布了文章 · 9月22日

18 张图,一文了解 8 种常见的数据结构

前几天和敖丙交流,他说我们写作的人都是在不停地燃烧自己,所以需要不停地补充燃料。对于他的观点,我不能再苟同了——所以我开始狂补计算机方面的基础知识,这其中就包括我相对薄弱的数据结构。

百度百科对数据结构的定义是:相互之间存在一种或多种特定关系的数据元素的集合。定义很抽象,需要大声地朗读几遍,才有点感觉。怎么让这种感觉来得更强烈,更亲切一些呢?我来列举一下常见的 8 种数据结构,数组、链表、栈、队列、树、堆、图、哈希表。

这 8 种数据结构有什么区别呢?

①、数组

优点:

  • 按照索引查询元素的速度很快;
  • 按照索引遍历数组也很方便。

缺点:

  • 数组的大小在创建后就确定了,无法扩容;
  • 数组只能存储一种类型的数据;
  • 添加、删除元素的操作很耗时间,因为要移动其他元素。

②、链表

《算法(第 4 版)》一书中是这样定义链表的:

链表是一种递归的数据结构,它或者为空(null),或者是指向一个结点(node)的引用,该节点还有一个元素和一个指向另一条链表的引用。

Java 的 LinkedList 类可以很形象地通过代码的形式来表示一个链表的结构:

public class LinkedList<E> {
    transient Node<E> first;
    transient Node<E> last;

    private static class Node<E> {
        E item;
        Node<E> next;
        Node<E> prev;

        Node(Node<E> prev, E element, Node<E> next) {
            this.item = element;
            this.next = next;
            this.prev = prev;
        }
    }
}

这是一种双向链表,当前元素 item 既有 prev 又有 next,不过 first 的 prev 为 null,last 的 next 为 null。如果是单向链表的话,就只有 next,没有 prev。

单向链表的缺点是只能从头到尾依次遍历,而双向链表可进可退,既能找到下一个,也能找到上一个——每个节点上都需要多分配一个存储空间。

链表中的数据按照“链式”的结构存储,因此可以达到内存上非连续的效果,数组必须是一块连续的内存。

由于不必按照顺序的方式存储,链表在插入、删除的时候可以达到 O(1) 的时间复杂度(只需要重新指向引用即可,不需要像数组那样移动其他元素)。除此之外,链表还克服了数组必须预先知道数据大小的缺点,从而可以实现灵活的内存动态管理。

优点:

  • 不需要初始化容量;
  • 可以添加任意元素;
  • 插入和删除的时候只需要更新引用。

缺点:

  • 含有大量的引用,占用的内存空间大;
  • 查找元素需要遍历整个链表,耗时。

③、栈

栈就好像水桶一样,底部是密封的,顶部是开口,水可以进可以出。用过水桶的小伙伴应该明白这样一个道理:先进去的水在桶的底部,后进去的水在桶的顶部;后进去的水先被倒出来,先进去的水后被倒出来。

同理,栈按照“后进先出”、“先进后出”的原则来存储数据,先插入的数据被压入栈底,后插入的数据在栈顶,读出数据的时候,从栈顶开始依次读出。

④、队列

队列就好像一段水管一样,两端都是开口的,水从一端进去,然后从另外一端出来。先进去的水先出来,后进去的水后出来。

和水管有些不同的是,队列会对两端进行定义,一端叫队头,另外一端就叫队尾。队头只允许删除操作(出队),队尾只允许插入操作(入队)。

注意,栈是先进后出,队列是先进先出——两者虽然都是线性表,但原则是不同的,结构不一样嘛。

⑤、树

树是一种典型的非线性结构,它是由 n(n>0)个有限节点组成的一个具有层次关系的集合。

之所以叫“树”,是因为这种数据结构看起来就像是一个倒挂的树,只不过根在上,叶在下。树形数据结构有以下这些特点:

  • 每个节点都只有有限个子节点或无子节点;
  • 没有父节点的节点称为根节点;
  • 每一个非根节点有且只有一个父节点;
  • 除了根节点外,每个子节点可以分为多个不相交的子树。

下图展示了树的一些术语:

根节点是第 0 层,它的子节点是第 1 层,子节点的子节点为第 2 层,以此类推。

  • 深度:对于任意节点 n,n 的深度为从根到 n 的唯一路径长,根的深度为 0。
  • 高度:对于任意节点 n,n 的高度为从 n 到一片树叶的最长路径长,所有树叶的高度为 0。

树的种类有很多种,常见的有:

  • 无序树:树中任意节点的子节点之间没有顺序关系。那怎么来理解无序树呢,到底长什么样子?

假如有三个节点,一个是父节点,两个是同级的子节点,那么就有三种情况:

假如有三个节点,一个是父节点,两个是不同级的子节点,那么就有六种情况:

三个节点组成的无序树,合起来就是九种情况。

  • 二叉树:每个节点最多含有两个子树。二叉树按照不同的表现形式又可以分为多种。

完全二叉树:对于一颗二叉树,假设其深度为 d(d > 1)。除了第 d 层,其它各层的节点数目均已达最大值,且第 d 层所有节点从左向右连续地紧密排列,这样的二叉树被称为完全二叉树。

拿上图来说,d 为 3,除了第 3 层,第 1 层、第 2 层 都达到了最大值(2 个子节点),并且第 3 层的所有节点从左向右联系地紧密排列(H、I、J、K、L),符合完全二叉树的要求。

满二叉树:一颗每一层的节点数都达到了最大值的二叉树。有两种表现形式,第一种,像下图这样(每一层都是满的),满足每一层的节点数都达到了最大值 2。

第二种,像下图这样(每一层虽然不满),但每一层的节点数仍然达到了最大值 2。

二叉查找树:英文名叫 Binary Search Tree,即 BST,需要满足以下条件:

  • 任意节点的左子树不空,左子树上所有节点的值均小于它的根节点的值;
  • 任意节点的右子树不空,右子树上所有节点的值均大于它的根节点的值;
  • 任意节点的左、右子树也分别为二叉查找树。

基于二叉查找树的特点,它相比较于其他数据结构的优势就在于查找、插入的时间复杂度较低,为 O(logn)。假如我们要从上图中查找 5 个元素,先从根节点 7 开始找,5 必定在 7 的左侧,找到 4,那 5 必定在 4 的右侧,找到 6,那 5 必定在 6 的左侧,找到了。

理想情况下,通过 BST 查找节点,所需要检查的节点数可以减半。

平衡二叉树:当且仅当任何节点的两棵子树的高度差不大于 1 的二叉树。由前苏联的数学家 Adelse-Velskil 和 Landis 在 1962 年提出的高度平衡的二叉树,根据科学家的英文名也称为 AVL 树。

平衡二叉树本质上也是一颗二叉查找树,不过为了限制左右子树的高度差,避免出现倾斜树等偏向于线性结构演化的情况,所以对二叉搜索树中每个节点的左右子树作了限制,左右子树的高度差称之为平衡因子,树中每个节点的平衡因子绝对值不大于 1。

平衡二叉树的难点在于,当删除或者增加节点的情况下,如何通过左旋或者右旋的方式来保持左右平衡。

Java 中最常见的平衡二叉树就是红黑树,节点是红色或者黑色,通过颜色的约束来维持着二叉树的平衡:

1)每个节点都只能是红色或者黑色

2)根节点是黑色

3)每个叶节点(NIL 节点,空节点)是黑色的。

4)如果一个节点是红色的,则它两个子节点都是黑色的。也就是说在一条路径上不能出现相邻的两个红色节点。

5)从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。

  • B 树:一种对读写操作进行优化的自平衡的二叉查找树,能够保持数据有序,拥有多于两个的子树。数据库的索引技术里就用到了 B 树。

⑥、堆

堆可以被看做是一棵树的数组对象,具有以下特点:

  • 堆中某个节点的值总是不大于或不小于其父节点的值;
  • 堆总是一棵完全二叉树。

将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。

⑦、图

图是一种复杂的非线性结构,由顶点的有穷非空集合和顶点之间边的集合组成,通常表示为:G(V,E),其中,G 表示一个图,V 是图 G 中顶点的集合,E 是图 G 中边的集合。

上图共有 V0,V1,V2,V3 这 4 个顶点,4 个顶点之间共有 5 条边。

在线性结构中,数据元素之间满足唯一的线性关系,每个数据元素(除第一个和最后一个外)均有唯一的“前驱”和“后继”;

在树形结构中,数据元素之间有着明显的层次关系,并且每个数据元素只与上一层中的一个元素(父节点)及下一层的多个元素(子节点)相关;

而在图形结构中,节点之间的关系是任意的,图中任意两个数据元素之间都有可能相关。

⑧、哈希表

哈希表(Hash Table),也叫散列表,是一种可以通过关键码值(key-value)直接访问的数据结构,它最大的特点就是可以快速实现查找、插入和删除。

数组的最大特点就是查找容易,插入和删除困难;而链表正好相反,查找困难,而插入和删除容易。哈希表很完美地结合了两者的优点, Java 的 HashMap 在此基础上还加入了树的优点。

哈希函数在哈希表中起着⾮常关键的作⽤,它可以把任意长度的输入变换成固定长度的输出,该输出就是哈希值。哈希函数使得一个数据序列的访问过程变得更加迅速有效,通过哈希函数,数据元素能够被很快的进行定位。

若关键字为 k,则其值存放在 hash(k) 的存储位置上。由此,不需要遍历就可以直接取得 k 对应的值。

对于任意两个不同的数据块,其哈希值相同的可能性极小,也就是说,对于一个给定的数据块,找到和它哈希值相同的数据块极为困难。再者,对于一个数据块,哪怕只改动它的一个比特位,其哈希值的改动也会非常的大——这正是 Hash 存在的价值!

尽管可能性极小,但仍然会发生,如果哈希冲突了,Java 的 HashMap 会在数组的同一个位置上增加链表,如果链表的长度大于 8,将会转化成红黑树进行处理——这就是所谓的拉链法(数组+链表)。

说句实在话,照这个进度恶补下去,我感觉要秃的节奏,不过,如果能够变得更强,值了——对,值了。还有一些小伙伴要我推荐一些算法和数据结构方面的书籍,我在 GitHub 上搜罗了一些比较受欢迎的,点击链接就可以下载,希望我的用心可以帮助到你。

链接:https://pan.baidu.com/s/1rB-CCjjpKPidOio7Ov_0YA 密码:g5pl

我是沉默王二,一枚沉默但有趣的程序员,关注即可提升学习效率。喜欢这篇文章的小伙伴,请不要忘记四联啊,点赞、收藏、转发、留言,你最美你最帅!

查看原文

赞 8 收藏 7 评论 0

沉默王二 发布了文章 · 9月21日

初入职场的小伙伴请注意,这 8 个坑不要再踩了

如果这个世界上有这样一瓶药水,喝下后能够立马回到十年前,回到我刚毕业参加工作那会,我一定会毫不犹豫地喝下去。因为这十年来,我走了太多的弯路,要不也不可能成为小伙伴们的“人生导师”哈。

当然了,在重走职场前,我一定会送自己 8 个锦囊,要不回去了等于白回去,对吧?咱不能把踩过的坑再踩一遍。

01、选择和努力同样重要

努力决定了人生的下限,选择才决定了人生的上限——十年前,我是不懂这点的,只知道,“苍天不负有心人,只要肯攀登”;十年后,我明白,攀登之前,得选择好攀什么。

我是幸运的,随了一小部分同学的波,去了苏州,最后找到了一份自认为还可以的工作。但假如让我重新来选的话,我会选择更大一点的城市,上海或者北京。

去大城市,当然不是奔着高昂的房价去的,没人会傻到那种程度,去,只有一个目的,就是——机会。人这一辈子,不会平步青云,一直走上坡路。但如果走的坡太矮,到了坡顶,很快就下来了。

如果坡是陡峭的,尽管冲下来的速度会很快,但坡长是足够的。这就会应了那句话,“瘦死的骆驼比马大”。

我有一个大学同学,叫海洋。哥们上学的时候学习就是最勤奋的,和我一样,去的也是苏州,只不过现在还在苏州,已经在那边买房了,并且年薪很诱人。

还有一个大学同学,叫小龙。哥们 2014 年的时候去了上海,工资直接是苏州时的 2 倍还要多,发展前景贼好,但 2016 年的时候“衣锦还乡”似地回了郑州,结果呢?工资减了一多半不说,累成狗的同时,是一眼望不到头的“只长年纪不涨薪”。

我已经在洛阳生活了六七年了,过得不算差吧,经常有小伙伴羡慕我,说我生活惬意,过得美滋滋。但说心里话,还是会后悔回洛阳得太早,没有去更大的城市拼一把,没有把自己的能力发挥得淋漓尽致,有点不甘心啊。

有一个因为写作认识的朋友,和我一样,出过书,之前在长沙做技术总监,前几天发信息给我说,“二哥,有机会来杭州啊,我安排,洗脚啥的没问题。”我就纳闷,哥们在长沙混得不差啊,怎么下这么大的决心,重新出发了呢?

要知道,他和我一样,结了婚,有了孩子,家里人都反对他去,去了就要在杭州那边买房,重新开始。但我支持他,能做出这个决定,他就是我心目中的英雄!

人这一辈子,就怕的就是留有遗憾,他去杭州那边,薪资直接翻了翻,这是广阔无垠的天地,以他的能力,能够闯荡出新的成绩,这一点我是深信不疑的。

与其在长沙不温不火,真不如选择重新出发,去杭州燃烧一把。

我之前提到过,参加工作的第二年,女朋友放弃郑州大学的研究生保送名额,去了上海考同济大学的建筑系研究生。虽然最后遗憾差了几分没有考上,但如果重新来选的话,她说,“我还是会做出同样的选择。”

我佩服她的勇气。同时,如果再给我一次机会的话,我一定会给她提供一个更好的住宿环境,让她心无旁骛的考研,以她的学习能力,一定能考得上。她留在上海,而我也会选择去上海打拼。

以我们两个人的能力,在上海一定会比在洛阳好,哪怕是在上海混得不好迫不得已最后回洛阳,也会比没有在上海待过好很多,我是有这方面的蜜汁自信的。

退一万步说,我们去苏州的这些同学,远比毕业后留在郑州的过得好。每次有同学结婚,大家聚在一块的时候,留在郑州的同学就这样感慨:“还是你们这群去苏州的明智啊!”

明智啥呀,我们只是莫名其妙做了一个选择而已。

02、领导让你上就上

之前收到过一个小伙伴的私信,说,“二哥,有个领导离职了,于是领导的领导就临时决定让我上,但我自认为能力还不到,有点犹豫不决,怕做不好,怎么办呢?”

还能怎么办?上呗!

机会虽然是留给有准备的人,但更是留给那些领导肯器重的人啊。想一想,是不是这个道理。假如你的领导不走的话,你有机会出头?你领导的领导能让你上?这话虽然粗俗了点,但是真理啊。

体育场上有很多名不见经传的小将,因为主力受伤,临时被派上场,然后,然后就爆发出了惊人的潜力,等主力养伤回来后,发现已经没有了位置。

小将上场前,总不能给教练说,“教练,我没准备好,你换下一个人上吧!”假如这样的话,这小将就永远只能是板凳球员了,一辈子也没有出头之日。

在职场上,也是同样的道理,领导让你上就上。假如你在领导眼里没有位置的话,也不会让你上,他一定是发现了你身上其他同事没有的优点。

只有上了,才有做事的机会,才有机会肩负起更重要的职责。李诞知道吧?以前就是个幕后编剧,被迫走到了台前,结果火得一塌糊涂。火的结果,就是广告无数,赚钱赚到手软。

有小伙伴担心说,万一做不好,替领导背锅了,不就竹篮打水一场空了。瞧瞧这前怕虎后怕狼的,领导还怕被你拉下水呢?想啥呢?遇到自己不懂的,多和领导沟通交流反馈就行了。领导不是吃素的,选择你有他选择的理由,这一点,咱就不替领导瞎操心了。

我之前也提到过,工作的第二年,就被提拔做了 Team Leader,比公司很多学历高的同事都晋升得快。我那时候就特别好奇,心想,领导难道是发现了我身上某些优点,虽然我自己都没发现?

尽管有些担忧,但最后还是硬着头皮上了,给新人培训啊(话说我还是个新人呢),研究源码啊,做代码校审啊,攻坚技术难点啊,一年多时间下来,发现成长特别特别快,和我一块来的那些同事再也没赶上我的脚步。

03、不要过早离开一线

我 24 岁就回洛阳了,说实话,回头再看的话,有点过早了。考研的小伙伴可能 24 岁还没有毕业,对吧?

24 岁的年纪,正是打拼职场的青春年华啊。我有两个好朋友,一个叫庆哥,一个叫小鹿,小伙伴们应该在留言区经常看到他们的身影。他们俩今年差不多也是 24 岁的年纪,庆哥去了杭州,小鹿去了北京。你瞧,我和他们之间是多么大的反差。

回三线城市洛阳是有好处的,比如说房价低(不觉得),消费标准低(不觉得),生活节奏慢(不觉得)。但弊端更多,比如说工作机会少,你看我就很少提在洛阳的职场,因为乏善可陈,真的是。

作为程序员的我们,应该很清楚,互联网是联通世界各地的,我们村的大爷大妈们都会抖音直播。但是,互联网是有地域差别的,我们村就没有软件开发的工作。

小鹿之前说,他找工作时投了几十份简历。我回洛阳那会,就没有投几十份简历的机会,大概投了四五份吧,就觉得(可以去掉)没公司可投了。

有不少小伙伴问过我,“二哥,洛阳有没有好的工作机会啊,想回去,在外面漂时间久了,累。”说实话,在洛阳,做 C++ 的,有一家公司待遇还不错,我可以内推,但做 Java 的好公司寥寥无几。

我在洛阳过得不错,是有原因的。第一,我技术还过得去,也肯学习,肯输入,没有掉队;第二,我会写作,有一定的影响力,和大厂程序员有交流切磋的机会。小伙伴们可以羡慕我的生活,但也要看到我背后付出的努力(我四点多就起来写这篇文章了)。

04、只全栈不纵深要不得

我 2019 年的时候出版过一本黄皮书,名叫《Web 全栈开发进阶之路》,这本书的稿子早在 2016 年就动笔写了,只不过出版的进度比较慢。说实话,我现在不太喜欢提这件事。因为“全栈”就意味着什么技术都会,但又什么都不精通。

想想是这个道理。为什么说大厂的程序员都是一颗螺丝钉,只需要负责自己擅长的就够了?因为不需要面面俱到啊,大厂讲究的是团队的配合,前端干前端的事,后端干后端的事,前后端又可以细分出很多领域,每个人只需要把自己手上的活干好,干明白就完事了。

小公司没有那么多人力,所以一个开发要肩负起很多的职责。往往一个项目的开发,从需求沟通,到产品设计,到代码研发,到测试,到运维,到后期维护,基本上是一肩挑。

人的时间和精力是有限的,干得多了,就没办法深入研究一个领域,做到专家的程度。与此同时,不可替代性就降低了。

关注我比较久的小伙伴应该可以看得到,我这一年多时间里,一直在 Java 的领域深耕,研究得越深,就越发现,可写的内容越来越多;甚至有些话题,每研究一次,就能发现一些新的技术细节。

我第一次阅读 HashMap 的源码时,了解到 HashMap 难的不是 Map 而是 Hash;第二次阅读的时候,了解到 HashMap 是通过拉链法解决的哈希冲突;第三次阅读的时候,了解到 HashMap 里不只有数组和链表,还有红黑树;第四次阅读的时候,了解到红黑树可以提高链表的查询效率。

纵深,其实是战略上的一个用词,指的是军队作战地域纵向的深度。深度的量决定了防御体系的坚固程度和攻击体系的出击强度,是近代战争立体化的体现。这个词,也可以用到职场上,那就是我们不要一味追求技术的覆盖面,更应该注重技术的纵深度

尤其是进入职场的前五六年,一定要纵得深一点,这样才能安身立命。等到这个技术壁垒建立了以后,随着工作经验的累计,就可以在广度上花一些功夫了,因为技术是要更新迭代的。

05、趁早打造影响力

JavaGuide, 应该有不少小伙伴认识,他早在大三的时候就维护一个叫“JavaGuide”的开源项目,截止到目前,这个项目在 GitHub 上的 star 数已经超过 88k 了,排名非常靠前。这个项目还衍生出了一份内容非常棒的 PDF,名叫《JavaGuide 面试突击》,我前前后后看了两遍,真的是感慨良多:要出名,趁早啊!

像 Guide 哥这样,不仅建立了影响力,还真真正正地帮助了他人,自己优秀的同时带着他人一块优秀,才是真正的优秀啊。

按照往年这个时候,金九银十,正是找工作的黄金档期,为了小伙伴们着想,我把这份 PDF 的下载地址放到了百度网盘,小伙伴们顺带下载一波,我也趁这个机会帮 Guide 哥宣传一波。

链接:https://pan.baidu.com/s/1S_qZ... 密码:369n

06、精通一门外语

我在苏州的时候,是在一家日企,但不会日语,吃了很大的亏。第一次去日本出差的机会就是因为这个泡汤的,说起来后悔死了。

当时护照都办好了,特意从苏州跑回户口所在地洛宁办的。结果领导临时决定,让另外一个同事替我去,因为同事的日语比我好一些。

小伙伴们可能有所不知,在日企,去日本出差可是一项美差——不光这边的工资照发,那边还有相当高额的补贴,基本上去一趟,一年的奖金就赚回来了。

我在技术上是没法挑剔的,这个领导心里一清二楚,毕竟项目的核心代码都是我带着团队写的。可我那时候就是讨厌学日语,提不起半点学日语的兴趣。

由于每个项目组都会配备一名专职的翻译小姐姐,所以遇到看不懂的文档我都会找她们翻译,况且日常工作中还有一款非常强大的翻译软件——灵格斯词典。日语中有不少汉字,只要掌握一些语法,结合着翻译词典,基本上的意思都能看得大差不差。

于是呢,每周的日语课我也不怎么花心思。上课的老师都是平常工作时候的翻译小姐姐,关系很熟,每当我被提问的时候,我都会穷尽心思用蹩脚的日语造个句子调戏一下小姐姐。

后来从日企离职了,确实用不着日语了,但偶尔看一些动漫的时候还是会后悔,要是懂日语的话,就知道他们究竟在说些什么,不用再翻译成中文了。

日语不好,问题还不算太大。但如果英语不好的话,对于程序员来说,就是个巨大的劣势。因为技术上的一手资料,大多数时候来自于外文网站。

如果你想在程序员这条路上走到黑的话,抓紧时间把英语能力提上去。

07、基础还是要扎实

不害臊地说,我就吃了很多基础知识薄弱的亏,以至于最近一段时间,不得不疯狂地补。《一文了解 8 种数据结构》这篇文章我写了差不多 3 天时间,写完后真的感觉自己在这方面进步很大。

计算机基础知识包括:算法和数据结构、计算机操作系统、计算机网络、计算机组成原理等等。这些基础知识,就像我们的内功,如果在未来想要走的更远,这些内功是必须要修炼的。

技术是层出不穷的,框架是千变万化的,但那些通用的底层知识是亘古不变的,掌握了这些基础知识,不仅可以帮助我们更快地学习一门新的语言,还能让我们在性能方面做出更好的优化。

算法的思维导图如下所示:

数据结构的思维导图如下所示:

计算机操作系统的思维导图如下所示:

计算机网络的思维导图如下所示:

计算机组成原理的思维导图如下所示:

有些小伙伴可能还在上大学,觉得学校的计算机专业课程比较落后,比较枯燥,但这些基础课程还是要好好学的。也许上学的时候感觉不到有多大的用处,但实际开发工作中,基础知识的扎实程度决定了一名程序员的上限。

很多大点的互联网公司无论校招还是社招,就喜欢考察这些基础知识。此外,很多平时开发中用到的技术都会涉及到这些基础知识,比如说为了提高查询性能需要使用的缓存技术。

我把这些思维导图以及计算机基础知识方面的资料整理到了下面这个下载地址里:

链接:https://pan.baidu.com/s/1easO... 密码:ombj

08、好好保重身体

提起程序员,总免不了和一些段子关联上,比如说“要变强,必变秃”,再比如说:

零基础学编程→某编程语言入门→某编程语言进阶→技术专家→颈椎病

这些段子听上去是不是莫名有一股心酸,对于大多数程序员来说,生活没有那么多诗和远方,只有加不完的班,写不完的需求和改不完的 bug

这篇文章,写了差不多七八个小时,写最后这段时真的快撑不住了,背部和颈椎这块,特别疼。

小伙伴们平常也要抽时间锻炼会,真的,咱们不像人家张朝阳了,张康阳了,每天只需要睡四个小时就足够了,我觉得他们(可能)不是人。

古人有句话说得好,“身体发肤,受之父母,不敢损伤,孝之始也。”我们的身体不仅仅是自己的,还是父母和爱你的人的,所以如果能够回到十年前,我那时候就开始锻炼,决不懈怠。

我是沉默王二,一枚沉默但有趣的程序员,关注即可提升学习效率。喜欢这篇文章的,请不要忘记四联啊,点赞、收藏、转发、留言,你最美你最帅!

查看原文

赞 3 收藏 2 评论 1

沉默王二 发布了文章 · 9月14日

数据结构与算法的正确学习姿势,10 本优质书单推荐

有个读者 diao 要我推荐数据结构和算法方面的书,我觉得很有必要给大家普及一下,因为算法和数据结构实在是太特么重要了——就好像我们人类离不开氧气,绿色植物离不开二氧化碳一样!

请肆无忌惮地点赞吧,微信搜索【沉默王二】关注这个在九朝古都洛阳苟且偷生的程序员。

本文 GitHub github.com/itwanger 已收录,里面还有我精心为你准备的一线大厂面试题。

除了 diao,还有个妹子在后台留言给我,也要推荐一波数据结构和算法方面的书籍:

鉴于此,我没吃没喝耗了两天的时间,终于整理好了。除此之外,我还充了百度网盘的会员和 CSDN 的会员(好下载资源),也问了好几个数据结构和算法方面的专家,好验证我的书单是否值得信赖——他们异口同声的肯定了我的付出。

可能有些读者会有这样的疑惑,数据结构有什么用?学习算法有必要吗?那我先来装模作样的回答一下这两个问题。

在计算机领域,通常要处理这样的问题:

1)如果将数据存储到计算机当中。

2)用什么方法来解决这个问题。

数据是一切能输入到计算机中的信息综合,结构是指数据之间的关系,那数据结构就是将数据和它们之间的关系存储到计算机当中。怎么实现存储呢?就需要选择合适的算法,效率才会更高。

Pascal 之父、结构化程序设计的先驱 Niklaus Wirth 有一本非常著名的书,叫作《算法 + 数据结构 = 程序》,可见,数据结构和算法对于程序设计来说,真的非常重要。

数据结构和算法,就像操作系统和计算机网络一样,看似离我们很近,但似乎又很远。

之所以近,是因为如果不懂数据结构和算法,基本上面试就过不了,不管是校招还是社招。之所以远,是因为实际工作中,如果不涉及到操作系统、搜索引擎、网络调度等等方面的底层业务,如果不考虑性能,似乎根本用不到,只要把编程语言封装好的 API 调用得当,只要把框架用的熟练,照样能把代码写得 66 的。

作为一名在编程领域摸爬滚打了十多年的老鸟,我必须郑重其事地提醒一下在座的各位。

如果你是大学生,一定要学习数据结构和算法,否则面试碰壁的时候你会后悔的,除非你打算在小公司混一辈子。

如果考研的话,数据结构也是必考科目。

如果你已经参加工作,想要摆脱 CRUD 的标签,也一定要学习数据结构和算法,否则只能停留在助理工程师和工程师的阶段,无法更进一步。

除此之外,掌握数据结构和算法,还有助于阅读源码和理解其背后的设计思想。

明白了数据结构和算法的重要性之后,我相信读者朋友们已经迫不及待、摩拳擦掌、跃跃欲试了,“请告诉我们该阅读哪些书籍吧!”

第一本,《大话数据结构》

《大话数据结构》 这本书最大的特点是,它把理论讲得很有趣,不枯燥。读技术书最大的烦恼不是这本书经典不经典,而是能不能看的进去,能看的进去,学到了,这本书就是好书。如果看不进去,哪怕是再经典的书,对学习的能都没有一丁点的帮助,对吧?

网络上对这本书的评价褒贬不一,但总体销量还是很不错的,作者也是一名老程序员了。书中的示例用的 C 语言。

第二本,《算法图解》

就像《算法图解》(代码使用 Python 语言实现的)这本书副标题写的那样,“像小说一样有趣的算法入门书”,主打“图解”,通俗易懂,学习起来就轻松多了,对吧?

通过《大话数据结构》和《算法图解》两本书的学习,我相信读者朋友们一定能够入门数据结构和算法了。如果还想更系统、更深入地学习,请继续往下看。

第三本,《数据结构和算法分析》

黑皮书,一眼看上去,就知道是一本经典书,对吧?《数据结构和算法分析》这本书的作者也非常用心,例子不仅有 Java 版的,还有 C 版和 C++ 版的。

这就解决了很多读者朋友们的烦恼,我不擅长 C 啊,我就想看 Java 版的,读者 giao 就要求我给他推荐一些 Java 版的书籍。

第四本,《剑指 offer》

这本书剖析了 80 个典型的编程面试题,如果能搞懂这本书里的内容,应付一般公司的面试应该不成问题。

直白点说,学习算法和数据结构会非常辛苦,那既然付出了这么多心血,我们的目的就很明确,获得一份更好的工作岗位,《剑指 offer》这本书一定能够帮助到我们。

刷题的话,可以选择牛客网或者力扣,如果是 Java 程序员的话,用 Java 刷题就行了。

牛客网:

https://www.nowcoder.com/ta/coding-interviews

力扣:

https://leetcode-cn.com/problemset/lcof/

认认真真看完这四本书,如果能够一个一个例子做下来,我相信读者朋友们就可以拍着胸脯自信地说,“数据结构和算法,我算是拿下了!”

“一千个读者,就有一千个哈姆雷特”,不同的读者在读同一本书的时候,感受也是不同的。同理,一个读者在读同一个主题下不同的书时,收获也会大有不同。

那我觉得,很有必要再推荐一些其他方面的书,供不同的读者选择。

第五本,《趣学数据结构》

讲解比较生动,用 C++ 描述的,适合基础一般的初学者。作者陈小玉是我们河南的,还写过另外一本算法方面的书,同样很适合初学者。

第六本,《啊哈算法》

一本有趣的算法入门书,C 语言实现的,没有枯燥的描述,没有难懂的公式,一切以实际应用为出发点。

第七本,《漫画算法:小灰的算法之旅》

用漫画的形式讲述了算法和数据结构的基础知识、复杂多变的算法面试题目及算法的实际应用场景。出了两版,一版 Python,一版 Java。

第八本,《程序员代码面试指南:IT 名企算法与数据结构题目最优解》

这是一本程序员代码面试"神书”!书中对 IT 名企代码面试各类题目的最优解进行了总结,并提供了相关代码实现,选取将近 300 道真实出现过的经典代码面试题,"刷”完这书,就是"题王”!

同样可以上牛客网上刷题:

https://www.nowcoder.com/ta/programmer-code-interview-guide

第九本,《算法》

这是一本非常适合于自学以及作为教材的算法书,特点有:基础非常全面、图示清晰易懂、数学要求低。代码是通过 Java 实现的,虽然是一本大部头书,但难懂的话不过。

第十本,《数据结构与算法之美》

严格意义上说,这不是一本书,它是 XX 时间(虽然很隐晦,但还是打钱吧)平台推出的付费栏目。推荐原因我就不多说了,书籍看累了,是一种选择。

就先推荐这十本吧,够大家学习一段时间了。最后,再来说一说学习数据结构和算法的方法吧,简单来说,就两点:

1)选择一本合适的书

这个问题,我已经帮大家解决了,不论你选择哪一本,最重要的是开始,不要犹豫,早就是优势。

2)编程实现和应用

理解不代表会用,对吧?只有自己亲自动手去实现,去反复的练习,才能真正地掌握。第一次练习可能不记不住,那就第二次、第三次,不要急躁,给自己一点时间和耐心。

如果你手里有点闲钱,建议直接购买纸质书阅读;如果手头确实紧张,钱都给对象买礼物了,那下面这个百度链接也许可以缓解一下你的资金压力:

下载链接:https://pan.baidu.com/s/1rB-C... 密码:g5pl

最后,希望二哥这些诚恳的建议能够给大家一点点帮助!love peace and sleep!

查看原文

赞 3 收藏 2 评论 0

沉默王二 发布了文章 · 9月11日

二哥来扫盲了:Java 后端开发常用的 10 种第三方服务

请肆无忌惮地点赞吧,微信搜索【沉默王二】关注这个在九朝古都洛阳苟且偷生的程序员。

本文 GitHub github.com/itwanger 已收录,里面还有我精心为你准备的一线大厂面试题。

严格意义上说,所有软件的第三方服务都可以自己开发,不过从零到一是需要时间和金钱成本的。就像我们研发芯片,投入了巨大的成本,但仍然没有取得理想的成绩,有些事情并不是一朝一夕,投机取巧就能完成的。

Java 后端开发通常会涉及到很多第三方服务,那么都有哪些成熟的方案可供直接上手使用呢?

1)IaaS

IaaS 的英文全称是 Infrastructure as a Service,即基础设施服务,指把 IT 基础设施作为一种服务通过网络对外提供,并根据用户对资源的实际使用量或占用量进行计费的一种服务模式。IaaS 可根据需求快速纵向扩缩,用户无需购买和管理自己的实体服务器和其他数据中心基础结构,从而避免了相应的开支和复杂操作。

用户通过 IaaS 可以完成的典型事项包括:

  • 测试和开发。
  • 网站托管。
  • 存储、备份和恢复。
  • Web 应用。
  • 高性能计算。
  • 大数据分析。

基本上所有的云服务商都提供了 IaaS 服务,国内最强大的云服务商当属阿里云。

2)PaaS

PaaS 的英文全称是 Platform as a Service,只需要提交代码到指定运行环境,代码打包、部署、IP 绑定都由平台完成。

与 IaaS 相比,用户不需要管理与控制云端基础设施(包含网络、服务器、操作系统或存储),但需要控制上层的应用程序部署与应用托管的环境。

3)SaaS

SaaS 的英文全称是 Software as a Service,用户在这种模式下,不需要经过传统的安装步骤就可以通过网络使用软件。SaaS 最大的特色在于软件本身并没有被下载到用户的硬盘,而是存储在提供商的云端或者服务器。

怎么区分 IaaS、PaaS 和 SaaS 呢?来看下面这张图。

如果我们开发了一个网站,按照传统的方式,我们需要买专业的服务器(连接网络),并在上面安装服务器软件,然后再把编写好的网站部署上去。

如果采用 IaaS 服务的话,就不需要自己购买服务器了,直接在租用的云服务器上安装服务器软件并且部署网站即可。

如果采用 PaaS 服务的话,不需要购买服务器,也不需要安装服务器软件,只需要部署网站即可。

如果采用 SaaS 服务的话,网站也不需要自己开发了,直接使用服务商开发好的网站,后期的升级、维护都交由服务商来负责。

阮一峰的网络日志上这样解释三者之间的关系。假如你想做披萨生意,有三种方案。

方案一,IaaS。

他人提供厨房、炉子、煤气,你使用这些基础设施,来烤你的披萨。

方案二,PaaS。

除了基础设施,他人还提供披萨饼皮。你只需要把自己的配料洒在饼皮上,至于是牛肉味的还是奥尔良鸡翅味的,你来决定。

方案三,SaaS。

他人直接做好了披萨,你拿到手就是一个成品。你要做的就是把披萨卖出去,最好印上自己的 Logo。

4)域名

有了可以提供服务的应用后,还需要一个能够让人记得住的域名,最好越简单越好。拿维基百科来说,wikipedia.org 是一个域名,和 IP 地址 208.80.152.2 相对应,用户可以直接访问 wikipedia.org 来代替 IP 地址,域名系统(DNS)会将域名转化成便于机器识别的 IP 地址。

有一段时间,域名炒得很厉害,就像炒楼盘一样。我有个大学同学就屯了不少域名,不过最终都没有卖出去。

小米联合创始人黎万强之前在微博上确认了小米域名(mi.com)的交易价格,360 万美元,约合人民币 2243 万元左右。雷军直呼“贼贵”!

我是通过腾讯云买的域名。

http://www.itwanger.com/

it 即 information technology,wanger 就是“沉默王二”中“王二”的拼音。不过说真的,后悔买 .com 了,年费有点贵,应该换成冷门的域名,比如说 .top,太穷了我。

5)CDN

CDN 的全称是 Content Delivery Network,即内容分发网络,一种透过互联网互相连接的电脑网络系统,利用最靠近每位用户的服务器,更快、更可靠地将音乐、图片、影片、应用程序及其他文件发送给用户,来提供高性能、可扩展性及低成本的网络内容传递给用户。

比较有名的 CDN 服务商有:

  • Cloudflare,提供的免费版解决方案足以保护小网站免受 DDoS 之灾,也可以隐藏网站的真实 IP 地址。在海外有着极好的响应速度,国内好像不咋滴。
  • 腾讯云,资源储备遍布全球 50+ 国家与地区,全网带宽 120Tbps+。国内 1100+ 加速节点,覆盖移动、联通、电信及十几家中小型运营商。
  • 阿里云,全球 2800+ 节点,130T 带宽能力,六大洲覆盖,国内主流运营商支持。
  • 七牛云,全球 2000 节点,20+ 运营商覆盖,访问提速 80%,应用场景包括音视频点播、大文件下载、Web 加速服务等。
  • 又拍云,全球 1100+ 节点,10Tbps 带宽储备,国内主流运营商支持。

我个人在做网站的时候,喜欢用 BootCDN 来加速 CSS 和 JavaScript,记忆里简介上说是又拍云和 Bootstrap 中文网合作的,现在看是猫云——不知道发生了什么。

6)邮件发送

基本上每个应用都离不开邮件发送,最常用的邮件服务器就是腾讯邮箱和网易邮箱。常用的电子邮件协议包括 SMTP、POP3 和 IMAP,不过,邮件的创建和发送只需要用到 SMTP 协议就可以了。

Java 官方提供了对电子邮件协议封装的 Java 类库,就是 JavaMail,但并没有包含在标准的 JDK 中,GitHub 地址如下:

https://javaee.github.io/javamail/

记得之前接到过这样一个需求,要求发送的邮件不能到垃圾邮箱里,还挺难做的。因为邮件服务器,比如说腾讯和网易,都做了邮件的过滤器,会识别一些邮件,自动放到垃圾邮箱里。

7)短信发送

使用短信发送验证码几乎是每个应用必不可少的一部分,仿佛手机号码就代表了一个人,所以丢手机是一件非常危险的事情——需要立即挂失。

短信是需要运营商支持的,所以基本上都需要依赖第三方代理。市面上有很多短信网关代理,阿里云通信是比较常用的一个,以前叫阿里大于。

阿里云通信的价格是每条短信 0.036 元,市面上还有一些其他的服务商,有些价格更低,但稳定性我个人没有测评过。我的技术交流群里就潜藏了不少做短信的商户人员。

应用接入阿里云通信并不复杂,我之前在 CSDN 上分享过一个博客,很详细,图文并茂,还带源码实例,需要的小伙伴可以去围观下。

https://qingmiaogu.blog.csdn.net/article/details/78751698

8)消息推送

消息推送(Push)指运营人员通过自己的产品或第三方工具对用户移动设备进行的主动消息推送。用户可以在移动设备锁定屏幕和通知栏看到 push 消息通知,通知栏点击可唤起 APP 并去往相应页面。

移动应用上,推送已经成为一个标配功能。

iOS 在系统层面与苹果 APNs(Apple Push Notification service)服务器建立连接,应用通过观察者模式向 iOS 系统注册关注的消息,系统收到 APNs Server 消息后转发到相应的应用程序。

Android 的 C2DM(Android Cloud to Device Messaging)采取与 iOS 类似的机制,都是由系统层面来支持消息推送,但是由于 Google 的服务在国内不能稳定的访问,此方案对于国内用户来说基本是无法使用的。

鉴于 Android 平台 C2DM 推送的不可用性,国内涌现出大量的第三方推送服务提供商,目前应用最为广泛的第三方推送服务提供商包括个推、极光、友盟、小米、华为、BAT 等,我之前用个推做过一个小程序的推送 Demo,API 调用起来很简单,感觉还挺好用的。

消息推送有时候让人很烦,尤其是一些 APP,不停地推,所以我手机上的消息推送权限基本上是关闭状态的——从此世界就安静了。

9)开放平台

通过开放平台,可以使用 OAuth 等协议获取用户在第三方平台上的信息以实现第三方平台登录。比如用户想要登录 A 网站,A 网站让用户提供第三方网站的数据,证明自己的身份。获取第三方网站的身份数据,就需要 OAuth 授权。

国内的微博、微信、QQ 是最常见的第三方登录方式,阿里系的产品可以通过支付宝授权登录,还有一些网站绑定了 GitHub 登录。

阮一峰的网络日志上有一篇 GitHub OAuth 第三方登录示例教程:

https://www.ruanyifeng.com/blog/2019/04/github-oauth.html

10)支付接口

目前,接入最多的支付接口就是支付宝和微信。

支付宝提供了当面付、APP 支付、手机网站支付、电脑网站支付等支付接口。

提供的文档很齐全,还有 Java、PHP、.NET 等版本的 Demo。

https://opendocs.alipay.com/open/270

微信支付的话,我推荐使用开源工具库 WxJava:

https://github.com/Wechat-Group/WxJava

我们公司的网站就用的这个,支持包括微信支付、开放平台、小程序、企业微信/企业号和公众号等的后端开发,很齐全。

以上,希望对小伙伴们有所帮助,我们下期见。

二哥肝了两天两夜,《程序员不可或缺的软实力》第一版强势来袭,纯手敲,足足 20 万字精华文章,贯穿了我十余年的编程生涯,涉及到了生活和工作中的方方面面,如果你是迷茫的在校大学生,或者刚入职的新人,相信我的个人经历,可以给你带去一些思考,从而树立起正确的人生观和价值观。

那这份 PDF 该怎么获取呢?

百度云链接:https://pan.baidu.com/s/1o6MY84my0OD0DHnAmZT6rA 密码:tx5e

最后,真心希望这份 PDF 能够对大家起到实质性的帮助,我也会在后面不断完善这本电子书,敬请期待。

当然,也日常求个赞!

查看原文

赞 6 收藏 4 评论 0

沉默王二 发布了文章 · 9月9日

外包公司派遣到网易,上班地点网易大厦,转正后工资8k-10k,13薪,包三餐,值得去吗?

请肆无忌惮地点赞吧,微信搜索【沉默王二】关注这个在九朝古都洛阳苟且偷生的程序员。

本文 GitHub github.com/itwanger 已收录,里面还有我精心为你准备的一线大厂面试题。

题目很长,但映入眼帘的,只有两个字——不是“网易”,是“外包”了。

很想来谈谈这个话题,因为我已经被问过不下六十次这方面的问题:“二哥,面试上了一家公司,是外包的,听大家说‘千万不要去外包’,但现在找工作确实很难,我是继续刷面试题准备新的公司还是去外包呢?二哥能给提供一下建议吗?”

对于“外包”这个话题,我经常看到这样一些劝诫,“打死都不要去外包,外包就是食物链的最底层,不仅会遭受到正式员工的歧视,还学习不到真正的技术。”

就好像上学的年纪,经常听老师们这样的谆谆教导:“你们一定要好好学习啊,只有考上好大学,才能改变你们的命运。”

老师的话,作为学生的我们,是能够听得进去的,并没有一只耳朵进一只耳朵出。但能不能考到高分,上一所好大学,就看天赋和勤奋了。

事实上,每年高考落榜的大有人在,这是不争的事实。社会上的资源永远在向优秀的人倾斜,尽管我们是生活在倡导人人平等、公平竞争的环境里。

作为一个人,我们必须时时刻刻清醒地看待自己,做到不卑不亢才能坚强地活下去

我有很多糟糕的经历,很多朋友劝我不要说这些陈年往事,容易给自己的人设招黑,我自己到觉得无所谓。和读者朋友们交流,就应该心平气和,想说什么就说,不要遮遮掩掩。

我复读了一年,上了大专,学历上遭受过鄙视;大三的时候参加培训了俩月,履历上遭受过鄙视;正式参加工作的时候,签的不是正式员工的合同,对,就是外包,待遇上遭受过鄙视。

有过很长一段时间的自卑,有过很长一段时间的逃避,不太敢正视自己。怕脆弱的一颗心会受到伤害,所以有的时候特别敏感。担心领导是不是看不起自己,担心同事是不是没正眼看自己,担心交给自己的任务能不能顺利完成,担心喜欢的人是不是真的喜欢自己。。。。。。

一路就这么走过来,我始终怀着一颗虔诚的心,虚心向优秀的人学习,向他们靠拢,不断调整自己的方向,不断改变自己。

这么给你说吧,我们公司最低的学历也得是双一流的本科,研究生以上学历的居多,但你现在的收入已经超过了不少人。

这句话是我身边非常亲密的一个人给我说的,听起来虽然很魔幻,但确实是真的。

外包的确是这个社会上很残酷的一种现象,这是必须得承认的,这是一种赤裸裸的歧视

处在同一个屋檐下,却佩戴者不同颜色的工牌,不仅颜色不同,工牌的做工也完全不同。外包的明显质感很差,正式员工的明显上档次,一眼就能区分出来。关键是,工牌还必须得戴上!

红牌的看到蓝牌的心情可能是这样的:“你丫的外包的啊。”

蓝牌的还得做好心理建设:“咋地啦,看不起咋滴?你写的那些垃圾代码,bug 还是我修复的。”

有些歧视是隐蔽的,没有这么明目张胆。但我总觉得明着点来,比阴暗更好一点。

我当时虽然签的外包,但并没有愤愤不平,反而觉得是公司给了我一次重生的机会。毕竟全公司,大专学历的就那么三个,总得让公司给其他高学历的员工一点心理平衡。

自己确实也没有更好的选择,能找到一份工作——外包,也是对自己很好的一种肯定。

内心是羡慕那些红牌员工的,毕竟薪资起点高,福利待遇好,发展前景更明朗,走路都更自信,我自己也暗下决心,一定要通过自己的努力争取转正。

工作内容方面,和正式员工没觉得有什么分别,可能因公司而异,因能力而异吧。我自认为一点也不比正式员工差,甚至解决问题的能力比一些正式员工更加突出。

领导安排给我的任务和职责一开始确实比较低级,写 SQL,整理 Excel,但我觉得并不是因为是外包所以这样,而是因为我是新人,没有工作经验,确实没有能力去承担更重要的开发工作。

我整个人的姿态放得很低,有自卑的情绪,但知道自己几斤几两,知道应该去钻研源码,知道应该付出比正式员工更多的努力才能得到公司的认可。

这不是“外包”造成的,而是自己造成的。没有必要去抱怨,没有必要去纠结,没有必要去自怨自艾。

如果有能力,有更好的选择,谁不想成为正式员工。如果能力不够,就不要眼高手低,可以接受外包,但不要一辈子都是外包,不要心安理得,要把“外包”作为一种跳板

当然有不好的外包,就像有垃圾的大学一样,进去后只会浪费青春,浪费精力。一旦进入这种只会压榨劳动力,却无法成长的外包,就是跳进了火坑。

就题目来说,网易虽然有过名声很差的时候,但整体算是互联网大厂了,能够去网易学习到一线的技术,拓展一些眼界,结识一些人脉,我觉得是值得的。

并不是所有的正式员工都是趾高气扬的,瞧不起外包的;有些挺古道热肠的,愿意沟通,愿意交流,也愿意做朋友,我就遇到过这样的同事。所以,我对外包没有一点偏见。

想要转正,我觉得有必要做好下面几点:

1)努力工作。不管是黑猫还是白猫,总要逮住老鼠。如果正式员工解决不了的问题,外包员工却可以解决,我相信人生很快就会逆袭,对吧?

2)持续学习。一家公司,不管是好是坏,既然存在就有它的价值,我觉得不能非黑即白的下定义,外包就一定学习不到技术,公司用的什么技术很重要,更重要的是你愿意学习什么样的技术。

时间紧?不是问题,当前的大环境下,每个项目的时间都赶的挺紧的;技术烂,不是问题,我们可以学习新鲜的技术来对旧的代码进行重构。

如果抱怨说既没有时间,又没有精力可以学习,那不管是正式员工还是外包,都是没有前途的。

3)投资自己。不管处于什么样的环境下,哪怕糟糕到极限,也不要忘记投资自己。在人类历史上,有过特别黑暗的阶段,不仅物质上匮乏,精神上也匮乏,但总有一小撮人,他们坚持投资自己,那机会来的时候,出现转机的时候,他们就是那群熠熠生辉、光芒万丈的人。

网上看过这样一段话:

当你想要开车去周游世界时,并不需要给自己的车装满足够跑完整个世界的油量,而只需要加满第一箱油就可以了,路上有那么多加油站,你随时都可以加油,路上有那么多的人,你也不用所有的事都只靠自己,想要一箱油就跑完整个世界的人遍地都是,但他们可能永远都不会出发,只有那些真正经历过的风景,才会让人真正变得丰富起来,哪怕看风景时的你狼狈不堪。

很有道理,真的。外包并不是我们的归宿,它只是我们的一个跳板,在我们走投无路的时候,是可以选择的。

二哥肝了两天两夜,《程序员不可或缺的软实力》第一版强势来袭,纯手敲,足足 20 万字精华文章,贯穿了我十余年的编程生涯,涉及到了生活和工作中的方方面面,如果你是迷茫的在校大学生,或者刚入职的新人,相信我的个人经历,可以给你带去一些思考,从而树立起正确的人生观和价值观。

那这份 PDF 该怎么获取呢?

百度云链接:https://pan.baidu.com/s/1o6MY84my0OD0DHnAmZT6rA 密码:tx5e

最后,真心希望这份 PDF 能够对大家起到实质性的帮助,我也会在后面不断完善这本电子书,敬请期待。

当然,也日常求个赞!

查看原文

赞 4 收藏 2 评论 1

认证与成就

  • 获得 108 次点赞
  • 获得 1 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 1 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

  • 《Web 全栈开发进阶之路》

    全书一共7章,主要讲解了如何利用Maven快速地搭建SpringMVC的Web项目、jQuery的各种函数和方法调用、前端开发框架Bootstrap、如何对常见的jQuery和Bootstrap插件进行HTML扩展、关系型数据库MySQL及其连接方法、AdminLTE及其囊括的大量可直接投入项目使用的组件、From表单等内容。除此之外,本书包含大量源码实例,其均是完成的项目开发程序,并可在此之上进行二次开发,这样就能够帮助读者融会贯通,快速地完成一个企业级Web应用程序的设计,使读者在实战中学到技术的精髓。

注册于 2015-11-18
个人主页被 718 人浏览