5

爆肝四万字,写给Java零基础小白的自学路线和进阶指南,收藏能省掉几万学费。

 2 years ago
source link: https://blog.csdn.net/skylibiao/article/details/120754063
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

本文收录于《技术专家修炼》

文中配套资料合集 提取码:ar6f

路线导图高清源文件:地址

哈喽,大家好,我是一条~

根据群内粉丝的需求,一条爆肝一周整理了一套Java学习路线,有了方向才能按图索骥,事半功倍。

写给Java零基础小白的自学路线和进阶指南,收藏能省掉几万学费。

本文分为学习路线配套资料两部分。

0.贵在坚持

Java学习,如逆水行舟,不进则退。而自学,逆水还得加个水逆,难上加难。

所以我们要做好打持久战的准备。

按计划行事

凡事预则立,不预则废。一个好的计划是成功的一半,而这一半,一条已经帮你整理好了,你只需要收藏即可。

该路线图右侧为主路线,需循序渐进,步步为营;左侧为辅助路线,需贯穿始终,熟练掌握。

建议做好时间规划,不断的提高自己的学习效率,学习过程中尽量把手机调至静音给自己一个安静的学习环境和氛围。

驽马十驾,功在不舍。自学Java非一日之功,你知道的越多,不知道的也越多。所以,为自己找一个动力,为了改变命运,或是为了心爱的人,或是为了让别人高看一眼。男儿何不带吴钩,收取关山十五州。岁月无情,余生有涯,请将生活扛在肩上,只顾风雨兼程。

独脚难行,孤掌难鸣,一个人的力量终究是有限的,一个人的旅途也注定是孤独的。当你定好计划,怀着满腔热血准备出发的时候,一定要找个伙伴,和唐僧西天取经一样,师徒四人团结一心才能通过九九八十一难。

在学习过程中看下自己身边有没有Java这方面的大神,尽量多问,多交流,如果没有的话,来找我,我一定知无不言言无不尽,还可以给你找一群志同道合的人。水涨船高,柴多火旺,就是这个道理,闭门造车注定会半途而废。

1.Java基础

学习任何语言,都是先从他的基本语法开始,如果你有C语言的基础,会容易许多,没有也不用现学。

image-20211011130649245

基本数据类型

Java 语言提供了 8 种基本类型,大致分为 4 类(8位=1字节)

  • 整数型
    • byte - 1字节
    • short - 2字节
    • int - 4字节
    • long - 8字节,赋值时一般在数字后加上 lL
  • 浮点型
    • float - 4字节,直接赋值时必须在数字后加上 fF
    • double - 8字节,赋值时一般在数字后加 dD
  • 字符型
    • char - 2字节,存储 Unicode 码,用单引号赋值
  • 布尔型
    • boolean - 1字节,只有 true 和 false 两个取值,一个字节就够了

引用数据类型

简单来说,所有的非基本数据类型都是引用数据类型,除了基本数据类型对应的引用类型外,类、 接口类型、 数组类型、 枚举类型、 注解类型、 字符串型都属于引用类型。

主要有以下区别:

1、存储位置

  • 基本变量类型在方法中定义的非全局基本数据类型变量的具体内容是存储在栈中的
  • 引用数据类型变量其具体内容都是存放在堆中的,而栈中存放的是其具体内容所在内存的地址

2、传递方式

  • 基本数据类型是按值传递
  • 引用数据类型是按引用传递

面向对象三大特性

封装

1.什么是封装

封装又叫隐藏实现。就是只公开代码单元的对外接口,而隐藏其具体实现。

其实生活中处处都是封装,手机,电脑,电视这些都是封装。你只需要知道如何去操作他们,并不需要知道他们里面是怎么构造的,怎么实现这个功能的。

2.如何实现封装

在程序设计里,封装往往是通过访问控制实现的。也就是刚才提到的访问修饰符。

3.封装的意义

封装提高了代码的安全性,使代码的修改变的更加容易,代码以一个个独立的单元存在,高内聚,低耦合。

好比只要你手机的充电接口不变,无论以后手机怎么更新,你依然可以用同样的数据线充电或者与其他设备连接。

封装的设计使使整个软件开发复杂度大大降低。我只需要使用别人的类,而不必关心其内部逻辑是如何实现的。我能很容易学会使用别人写好的代码,这就让软件协同开发的难度大大降低。

封装还避免了命名冲突的问题。

好比你家里有各种各样的遥控器,但比还是直到哪个是电视的,哪个是空调的。因为一个属于电视类一个属于空调类。不同的类中可以有相同名称的方法和属性,但不会混淆。

继承

继承的主要思想就是将子类的对象作为父类的对象来使用。比如王者荣耀的英雄作为父类,后裔作为子类。后裔有所有英雄共有的属性,同时也有自己独特的技能。

多态

多态的定义:

指允许不同类的对象对同一消息做出响应。即同一消息可以根据发送对象的不同而采用多种不同的行为方式。(发送消息就是函数调用)

简单来说,同样调用攻击这个方法,后裔的普攻和亚瑟的普攻是不一样的。

多态的条件:

  • 父类引用指向子类对象

多态的好处:

多态对已存在代码具有可替换性。

多态对代码具有可扩充性。

它在应用中体现了灵活多样的操作,提高了使用效率。

多态简化对应用软件的代码编写和修改过程,尤其在处理大量对象的运算和操作时,这个特点尤为突出和重要。

Java中多态的实现方式:

  • 继承父类进行方法重写
  • 同一个类中进行方法重载

Java基础完整讲解

入门练习案例

《入门练习100例》

1.Java核心技术卷一、二

全书共14章,包括Java基本的程序结构、对象与类、继承、接口与内部类、图形程序设计、事件处理、Swing用户界面组件、部署应用程序和Applet、异常日志断言和调试、叙述方式深入浅出,并包含大量示例,从而帮助读者充分理解Java语言以及Java类型库的相关特性。

img

2.学习视频
image-20211012001250513

2.JavaWeb

本章难度不高,但也不可忽视。其中前端基础不需花过多时间,重点放在Tomcat上,会陪伴你整个Java生涯。
JavaWeb是用Java技术来解决相关web互联网领域的技术栈。Web就是网页,分为静态和动态。涉及 的知识点主要包括jsp,servlet,tomcat,http,MVC等知识。

HTTP网络请求方式

  • GET:最常用的方式,用来向服务器请求数据,没有请求体,请求参数放在URL后面。
  • POST:用于向表单提交数据,传送的数据放在请求体中。
  • PUT:用来向服务器上传文件,一般对应修改操作,POST用于向服务器发送数据,PUT用于向服务器储存数据。没有验证机制,任何人都可以操作,存在安全问题。具有幂等性。
  • DELETE:用于删除服务器上的文件,具有幂等性。同样存在安全问题。
  • HEAD:用HEAD进行请求服务器时,服务器只返回响应头,不返回响应体。与GET一样没有请求体,常用于检查请求的URL是否有效。
  • PATCH:对资源进行部分修改。与PUT区别在于,PUT是修改所有资源,替代它,而PATCH只是修改部分资源。
  • TRACE:用来查看一个请求,经过网关,代理到达服务器,最后请求的变换。因安全问题被禁用。
  • OPTIONS:当客户端不清楚对资源操作的方法,可以使用这个,具有幂等性。

GET和POST

  1. 作用不同:GET 用于获取资源,而 POST 用于传输实体主体。
  2. 参数位置不一样: GET 的参数是以查询字符串出现在 URL 中,而 POST 的参数存储在实体主体中。虽然GET的参数暴露在外面,但可以通过加密的方式处理,而 POST 参数即使存储在实体主体中,我们也可以通过一些抓包工具如(Fiddler)查看。
  3. 幂等性:GET是幂等性,而POST不是幂等性。(面试官紧接着可能就会问你什么是幂等性?如何保证幂等性?)
  4. 安全性:安全的 HTTP 方法不会改变服务器状态,也就是说它只是可读的。 GET 方法是安全的,而 POST 却不是,因为 POST 的目的是传送实体主体内容,这个内容可能是用户上传的表单数据,上传成功之后,服务器可能把这个数据存储到数据库中,因此状态也就发生了改变。

常见的网络状态码

网络状态码共三位数字组成,根据第一个数字可分为以下几个系列:

1xx(信息性状态码)

代表请求已被接受,需要继续处理。

包括:100、101、102

这一系列的在实际开发中基本不会遇到,可以略过。

2xx(成功状态码)

表示成功处理了请求的状态代码。

200:请求成功,表明服务器成功了处理请求。

202:服务器已接受请求,但尚未处理。

204:服务器成功处理了请求,但没有返回任何内容。

206:服务器成功处理了部分 GET 请求。

3xx(重定向状态码)

300:针对请求,服务器可执行多种操作。

301:永久重定向

302:临时性重定向

303:303与302状态码有着相同的功能,但303状态码明确表示客户端应当采用GET方法获取资源。

301和302的区别?

301比较常用的场景是使用域名跳转。比如,我们访问 http://www.baidu.com 会跳转到https://www.baidu.com,发送请求之后,就会返回301状态码,然后返回一个location,提示新的地址,浏览器就会拿着这个新的地址去访问。

302用来做临时跳转比如未登陆的用户访问用户中心重定向到登录页面。

4xx(客户端错误状态码)

400:该状态码表示请求报文中存在语法错误。但浏览器会像200 OK一样对待该状态码。

401:表示发送的请求需要有通过HTTP认证的认证信息。比如token失效就会出现这个问题。

403:被拒绝,表明对请求资源的访问被服务器拒绝了。

404:找不到,表明服务器上无法找到请求的资源,也可能是拒绝请求但不想说明理由。

5xx(服务器错误状态码)

500:服务器本身发生错误,可能是Web应用存在的bug或某些临时的故障。

502:该状态码表明服务器暂时处于超负载或正在进行停机维护,现在无法处理请求。

⚠️有时候返回的状态码响应是错误的,比如Web应用程序内部发生错误,状态码依然返回200

转发和重定向

上面提到了重定向,那你知道什么是转发吗?

A找B借钱,B没有钱,B去问C,C有钱,C把钱借给A的过程。

客户浏览器发送http请求,web服务器接受此请求,调用内部的一个方法在容器内部完成请求处理和转发动作,将目标资源发送给客户。

整个转发一个请求,一个响应,地址栏不会发生变化,不能跨域访问。

2.重定向

A找B借钱,B没有钱,B让A去找C,A又和C借钱,C有钱,C把钱借给A的过程。

客户浏览器发送http请求,web服务器接受后发送302状态码响应及对应新的location给客户浏览器,客户浏览器发现是302响应,则自动再发送一个新的http请求,请求url是新的location地址,服务器根据此请求寻找资源并发送给客户。

两个请求,两个响应,可以跨域。

Servlet

servlet是一个比较抽奖的概念,也是web部分的核心组件,大家回答这个问题一定要加入自己的理解,不要背定义。

servlet其实就是一个java程序,他主要是用来解决动态页面的问题。

之前都是浏览器像服务器请求资源,服务器(tomcat)返回页面,但用户多了之后,每个用户希望带到不用的资源。这时就该servlet上场表演了。

servlet存在于tomcat之中,用来网络请求与响应,但他的重心更在于业务处理,我们访问京东和淘宝的返回的商品是不一样的,就需要程序员去编写,目前MVC三层架构,我们都是在service层处理业务,但这其实是从servlet中抽取出来的。

看一下servlet处理请求的过程:

image-20210812201447743

Servlet的生命周期

Servlet生命周期分为三个阶段:

  • 初始化阶段 调用init()方法
  • 响应客户请求阶段  调用service()方法-àdoGet/doPost()
  • 终止阶段  调用destroy()方法

MVC与三层架构

三层架构与MVC的目标一致:都是为了解耦和、提高代码复用。MVC是一种设计模式,而三层架构是一种软件架构。

MVC

Model 模型

模型负责各个功能的实现(如登录、增加、删除功能),用JavaBean实现。

View 视图

用户看到的页面和与用户的交互。包含各种表单。 实现视图用到的技术有html/css/jsp/js等前端技术。

常用的web 容器和开发工具

Controller 控制器

控制器负责将视图与模型一一对应起来。相当于一个模型分发器。接收请求,并将该请求跳转(转发,重定向)到模型进行处理。模型处理完毕后,再通过控制器,返回给视图中的请求处。

三层架构

表现层(UI)(web层)、业务逻辑层(BLL)(service层)、数据访问层(DAL)(dao层) ,再加上实体类库(Model)

  • 实体类库(Model),在Java中,往往将其称为Entity实体类。数据库中用于存放数据,而我们通常选择会用一个专门的类来抽象出数据表的结构,类的属性就一对一的对应这表的属性。一般来说,Model实体类库层需要被DAL层,BIL层和UI层引用。
  • 数据访问层(DAL),主要是存放对数据类的访问,即对数据库的添加、删除、修改、更新等基本操作,DAL就是根据业务需求,构造SQL语句,构造参数,调用帮助类,获取结果,DAL层被BIL层调用
  • 业务逻辑层(BLL),BLL层好比是桥梁,将UI表示层与DAL数据访问层之间联系起来。所要负责的,就是处理涉及业务逻辑相关的问题,比如在调用访问数据库之前,先处理数据、判断数据。

session、cookie、token

首先我们要明白HTTP是一种无状态协议,怎么理解呢?很简单

夏洛:大爷,楼上322住的是马冬梅家吧?
大爷:马冬什么? 
夏洛:马冬梅。 
大爷:什么冬梅啊? 
夏洛:马冬梅啊。 
大爷:马什么梅?
夏洛:行,大爷你先凉快着吧。

这段对话都熟悉吧,HTTP就是那个大爷,那如果我们就直接把“大爷”放给用户,用户不用干别的了,就不停的登录就行了。

既然“大爷不靠谱”,我们找“大娘”去吧。

哈哈哈,开个玩笑,言归正传。

为了解决用户频繁登录的问题,在服务端和客户端共同维护一个状态——会话,就是所谓session,我们根据会话id判断是否是同一用户,这样用户就开心了。

但是服务器可不开心了,因为用户越来越多,都要把session存在服务器,这对服务器来说是一个巨大的开销,这是服务器就找来了自己的兄弟帮他分担(集群部署,负载均衡)。

但是问题依然存在,如果兄弟挂了怎么办,兄弟们之间的数据怎么同步,用户1把session存放在机器A上,下次访问时负载均衡到了机器B,完了,找不到,用户又要骂娘。

这时有人思考,为什么一定要服务端保存呢,让客户端自己保存不就好了,所以就诞生了cookie,下一次请求时客户段把cookie发送给服务器,说我已经登录了。

但是空口无凭,服务器怎么知道哪个cookie是我发过去的呢?如何验证成了新的问题。

有人想到了一个办法,用加密令牌,也就是token,服务器发给客户端一个令牌,令牌保存加密后id和密钥,下一次请求时通过headers传给服务端,由于密钥别人不知道,只有服务端知道,就实现了验证,且别人无法伪造。

JavaWeb完整讲解

工欲善其事必先利其器,集合就是我们的器。

ArrayList

底层实现

由什么组成,我说了不算,看源码。怎么看呢?

List<Object> list = new ArrayList<>();

新建一个ArrayList,按住ctrlcommand用鼠标点击。

    /**
     * The array buffer into which the elements of the ArrayList are stored.
     * The capacity of the ArrayList is the length of this array buffer. Any
     * empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
     * will be expanded to DEFAULT_CAPACITY when the first element is added.
     * 翻译
     * 数组缓冲区,ArrayList的元素被存储在其中。ArrayList的容量是这个数组缓冲区的长度。
     * 任何空的ArrayList,如果elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA,
     * 当第一个元素被添加时,将被扩展到DEFAULT_CAPACITY。
     */
    transient Object[] elementData; 

毋庸置疑,底层由数组组成,那数组的特点就是ArrayList的特点。

  • 由于数组以一块连续的内存空间,每一个元素都有对应的下标,查询时间复杂度为O(1)。好比你去住酒店,每个房间都挨着,房门都写着房间号。你想找哪一间房是不是很容易。
  • 相对的,一块连续的内存空间你想打破他就没那么容易,牵一发而动全身,所以新增和删除的时间复杂度为O(n),想像你在做excel表格的时候,想增加一列,后面的列是不是都要跟着移动。
  • 元素有序,可重复。可用在大多数的场景,这个就不需要过多解释了。

扩容

我们知道数组是容量不可变的数据结构,随着元素不断增加,必然要扩容。

所以扩容机制也是集合中非常容易爱问的问题,在源码中都可以一探究竟。

1.初始化容量为10,也可以指定容量创建。

    /**
     * Default initial capacity.
     * 定义初始化容量
     */
    private static final int DEFAULT_CAPACITY = 10;

2.数组进行扩容时,是将旧数据拷贝到新的数组中,新数组容量是原容量的1.5倍。(这里用位运算是为了提高运算速度)

private void grow(int minCapacity) {
  int newCapacity = oldCapacity + (oldCapacity >> 1);
}

3.扩容代价是很高得,因此再实际使用时,我们因该避免数组容量得扩张。尽可能避免数据容量得扩张。尽可能,就至指定容量,避免数组扩容的发生。

为什么扩容是1.5倍?

  • 如果大于1.5,也就是每次扩容很多倍,但其实我就差一个元素的空间,造成了空间浪费。
  • 如果小于1.5,扩容的意义就不大了,就会带来频繁扩容的问题。

所以,1.5是均衡了空间占用和扩容次数考虑的。

线程安全问题

怎么看线程安全?说实话我以前都不知道,看网上说安全就安全,说不安全就不安全。

其实都在源码里。找到增加元素的方法,看看有没有加锁就知道了。

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

        ensureCapacityInternal(size + 1);  // Increments modCount!!
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
        elementData[index] = element;
        size++;
    }

没有加锁,所以线程不安全

在多线程的情况下,插入数据的时可能会造成数据丢失,一个线程在遍历,另一个线程修改,会报ConcurrentModificationException(并发修改异常)错误.

多线程下使用怎么保证线程安全?

保证线程安全的思路很简单就是加锁,但是你可没办法修改源码去加个锁,但是你想想编写java的大佬会想不到线程安全问题?

早就给你准备了线程安全的类。

1.Vector

Vector是一个线程安全的List类,通过对所有操作都加上synchronized关键字实现。

找到add方法,可以看到被synchronized关键字修饰,也就是加锁,但synchronized是重度锁,并发性太低,所以实际一般不使用,随着java版本的更新,慢慢废弃。

public void add(E e) {
            int i = cursor;
            synchronized (Vector.this) {
                checkForComodification();
                Vector.this.add(i, e);
                expectedModCount = modCount;
            }
            cursor = i + 1;
            lastRet = -1;
        }

2.Collections

注意是Collections而不是Collection

Collections位于java.util包下,是集合类的工具类,提供了很多操作集合类的方法。其中Collections.synchronizedList(list)可以提供一个线程安全的List

对于Map、Set也有对应的方法

3.CopyOnWrite(写时复制)

写时复制,简称COW,是计算机程序设计领域中的一种通用优化策略。

当有多人同时访问同一资源时,他们会共同获取指向相同的资源的指针,供访问者进行读操作。

当某个调用者修改资源内容时,系统会真正复制一份副本给该调用者,而其他调用者所见到的最初的资源仍然保持不变。修改完成后,再把新的数据写回去。

通俗易懂的讲,假设现在有一份班级名单,但有几个同学还没有填好,这时老师把文件通过微信发送过去让同学们填写(复制一份),但不需要修改的同学此时查看的还是旧的名单,直到有同学修改好发给老师,老师用新的名单替换旧的名单,全班同学才能查看新的名单。

共享读,分开写。读写分离,写时复制。

在java中,通过CopyOnWriteArrayListCopyOnWriteArraySet容器实现了 COW 思想。

平时查询的时候,都不需要加锁,随便访问,只有在更新的时候,才会从原来的数据复制一个副本出来,然后修改这个副本,最后把原数据替换成当前的副本。修改操作的同时,读操作不会被阻塞,而是继续读取旧的数据。

    /** The lock protecting all mutators */
    final transient ReentrantLock lock = new ReentrantLock();

    /** The array, accessed only via getArray/setArray. */
    private transient volatile Object[] array;

源码里用到了ReentrantLock锁和volatile关键字,会在《资深程序员修炼》专栏中做全面深度讲解。

集合完整讲解

4.JVM

重点来了,Java程序员一定要深入研究的内容

垃圾回收是重点难,先理解了垃圾回收,才能理解调优的思路。

判断垃圾

判断是否是垃圾共有两种方法。引用计数法和可达性分析

1.引用计数法

非常好理解,引用一次标记一次,没有被标记的就是垃圾。

在堆中存储对象时,在对象头处维护一个counter计数器,如果一个对象增加了一个引用与之相连,则将counter++

如果一个引用关系失效则counter--。如果一个对象的counter变为0,则说明该对象已经被废弃,不处于存活状态,此时可以被回收。

2.引用计数的缺点

  • 无法分析循环引用问题

3.可达性分析

类似的树结构,从根结点出发,即GC root,把有关系的对象用一颗树链接起来

那么我们遍历这棵树,没遍历到的对象,就是垃圾

4.有哪些可以做GC Roots的对象?

  • 虚拟机栈(栈桢中的本地变量表)中的引用的对象
  • 方法区中的类静态属性引用的对象
  • 方法区中的常量引用的对象
  • 本地方法栈中JNI(Native方法)的引用的对象

回收算法

回收算法是垃圾回收的思想,回收器是垃圾回收的实现

1.标记-清除

两次遍历:

  • 不需要格外空间,适合回收对象较少的区域
  • 效率低,遍历两次,时间复杂度O(n^2)
  • 会有线程停顿,stop the world (STW)
  • 空间碎片,因为垃圾可能不是连续的,大量的空间碎片会导致提前GC,这也是最主要的问题。

2.标记-复制

将空间分为相等大小的两部分,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用的内存空间一次清理掉,这样一来就不容易出现内存碎片的问题。牺牲空间解决碎片问题。

  • 高效无碎片
  • 占用大量空间

3.标记-整理

同样是为了解决空间碎片提出,区别是通过牺牲时间的方式。

和标记-清除类似,不一样的是在完成标记之后,它不是直接清理可回收对象,而是将存活对象都向一端移动,然后清理掉端边界以外的内存。

  • 解决空间碎片的问题
  • 不浪费空间
  • 相对比较耗时

JVM完整讲解

1.深入理解Java虚拟机

附带读书笔记。

5.多线程

理解多线程,才能更好的理解框架源码,进行高并发的架构设计,重中之重。

并行和并发

并行:多个任务在同一个 CPU 核上,按细分的时间片轮流(交替)执行,从逻辑上来看那些任务是同时执行。

并发:多个处理器或多核处理器同时处理多个任务。

并发 = 两个队列和一台咖啡机。

并行 = 两个队列和两台咖啡机。

线程和进程

一个程序下至少有一个进程,一个进程下至少有一个线程,一个进程下也可以有多个线程来增加程序的执行速度。

守护线程是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。在 Java 中垃圾回收线程就是特殊的守护线程。

创建线程4种方式

  • 继承 Thread 重新 run 方法;

  • 实现 Runnable 接口;

  • 实现 Callable 接口。

synchronized 底层实现

synchronized 是由一对 monitorenter/monitorexit 指令实现的,monitor 对象是同步的基本实现单元。

在 Java 6 之前,monitor 的实现完全是依靠操作系统内部的互斥锁,因为需要进行用户态到内核态的切换,所以同步操作是一个无差别的重量级操作,性能也很低。

但在 Java 6 的时候,Java 虚拟机 对此进行了大刀阔斧地改进,提供了三种不同的 monitor 实现,也就是常说的三种不同的锁:偏向锁(Biased Locking)、轻量级锁和重量级锁,大大改进了其性能。

synchronized 和 volatile 的区别

volatile 是变量修饰符;synchronized 是修饰类、方法、代码段。

volatile 仅能实现变量的修改可见性,不能保证原子性;而 synchronized 则可以保证变量的修改可见性和原子性。

volatile 不会造成线程的阻塞;synchronized 可能会造成线程的阻塞。

synchronized 和 ReentrantLock 区别

synchronized 早期的实现比较低效,对比 ReentrantLock,大多数场景性能都相差较大,但是在 Java 6 中对 synchronized 进行了非常多的改进。

主要区别如下:

ReentrantLock 使用起来比较灵活,但是必须有释放锁的配合动作;

ReentrantLock 必须手动获取与释放锁,而 synchronized 不需要手动释放和开启锁;

ReentrantLock 只适用于代码块锁,而 synchronized 可用于修饰方法、代码块等。

volatile 标记的变量不会被编译器优化;synchronized 标记的变量可以被编译器优化。

synchronized 和 Lock 区别

synchronized 可以给类、方法、代码块加锁;而 lock 只能给代码块加锁。

synchronized 不需要手动获取锁和释放锁,使用简单,发生异常会自动释放锁,不会造成死锁。

lock 需要自己加锁和释放锁,如果使用不当没有 unLock()去释放锁就会造成死锁。

通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到。

1 .java并发编程的艺术

《Java并发编程的艺术》内容涵盖Java并发编程机制的底层实现原理、Java内存模型、Java并发编程基础、Java中的锁、并发容器和框架、原子类、并发工具类、线程池、Executor框架等主题,每个主题都做了深入的讲解,同时通过实例介绍了如何应用这些技术。

6.设计模式

好多人觉得设计模式模式,那是因为你学的还不够深入,还没有看过源码,所以我特意将设计模式往前放了。

建造者模式

官方定义

将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。

通俗解读

提供一种创建对象的方式,创建的东西细节复杂,还必须暴露给使用者。屏蔽过程而不屏蔽细节

类似建房子,只需要把材料和设计图纸给工人,就能建成想要的房子,不关注工人建房子的过程,但对于细节,我们又可以自己设计。

本文源码:建造者模式 提取码: vpqt

建议跟着一条学设计模式的小伙伴都建一个maven工程,并安装lombok依赖和插件。

并建立如下包目录,便于归纳整理。

image-20210920205525607.png

pom如下

    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.16.10</version>
    </dependency>

现在有一个手机的建造者,我要让它为我生产不用品牌和配置的手机。该怎么实现?

1.创建手机类

@Data
public class Phone {       
        //处理器
        protected String cpu;
        //内存
        protected String mem;
        //磁盘
        protected String disk;
        //屏幕大小
        protected String size;
}

2.创建建造者接口

//定义建造者的模板方法
public interface Builder {
    Phone phone = new Phone();
    void buildCpu(String cpu);
    void buildMem(String mem);
    void buildDisk(String disk);
    void buildSize(String size);

    default Phone getPhone(){
        return phone;
    }
}

3.创建Vivo手机的建造者

public class VivoPhoneBuilder implements Builder{
		//建造者细节的实现
    @Override
    public void buildCpu(String cpu) {
        phone.cpu=cpu;
    }

    @Override
    public void buildMem(String mem) {
        phone.mem=mem;
    }

    @Override
    public void buildDisk(String disk) {
        phone.disk=disk;
    }

    @Override
    public void buildSize(String size) {
        phone.size=size;
    }
}

4.创建测试类

public class MainTest {
    public static void main(String[] args) {
        VivoPhoneBuilder builder = new VivoPhoneBuilder();
        builder.buildCpu("888");
        builder.buildDisk("512");
        builder.buildMem("16");
        builder.buildSize("plus");
        Phone phone = builder.getPhone();
        System.out.println(phone);
    }
}

5.输出结果

如果我这时需要生产OPPO手机,只需新建一个OppoPhoneBuilder实现Builder接口即可。

相信大家在开发中都遇见过这样的代码,像链子一样可以一直调用下去。

那么如何实现链式建造者呢?

有以下两种方式:

1.修改返回值为Builder

public interface Builder {
    Phone phone = new Phone();
    // void 改为 Builder 同步修改实现类
    Builder buildCpu(String cpu);
    Builder buildMem(String mem);
    Builder buildDisk(String disk);
    Builder buildSize(String size);

    default Phone getPhone(){
        return phone;
    }
}

测试1

public class MainTest {
    public static void main(String[] args) {
        // ……

        VivoPhoneBuilder builder2 = new VivoPhoneBuilder();
        Phone phone1 = builder2
                .buildCpu("888")
                .buildDisk("512")
                .buildMem("16")
                .buildSize("plus")
                .getPhone();
        System.out.println("phone1:"+phone1);
    }
}

结果1

2.使用lombok

@Data
@Builder   //使用链式建造者
@NoArgsConstructor
@AllArgsConstructor
public class Phone {
   // ……
} 

测试2

public class MainTest {
    public static void main(String[] args) {
      
				// ……
      
        Phone build = Phone.builder()
                .cpu("888")
                .mem("16")
                .disk("512")
                .size("plus").build();
        System.out.println("builder:"+build);
    }
}

结果2

  • StringBuilder:append(); 给谁append呢?
    public AbstractStringBuilder append(String str) {
        if (str == null)
            return appendNull();
        int len = str.length();
        ensureCapacityInternal(count + len);
        str.getChars(0, len, value, count);
        count += len;
        return this;
    }
  • Swagger-ApiBuilder;
  • 快速实现:Lombok-@Builder

建造者模式提供了对于同一构建过程的不同表示,像流水线一样生产对象。对于新增的对象,只需要创建对应的建造者即可,不需要修改源代码。

lombok为我们提供了建造者模式的快速实现(@Builder),要应用到实际编码中。

更多设计模式

更多设计模式

7.SSM框架

这对于初学者来说,是一个坎,前几年学完这些,已经可以开始找工作了,所以恭喜你能坚持带这里,胜利就在前方。

ORM 框架?

ORM(Object Relation Mapping)对象关系映射,是把数据库中的关系数据映射成为程序中的对象。

使用 ORM 的优点:提高了开发效率降低了开发成本、开发更简单更对象化、可移植更强。

MyBatis 中 #{}和 的区别

#是预编译处理,{}的区别是什么?#{}是预编译处理,的区别是什么?#是预编译处理,{}是字符替换。 在使用 #{}时,MyBatis 会将 SQL 中的 #{}替换成“?”,配合 PreparedStatement 的 set 方法赋值,这样可以有效的防止 SQL 注入,保证程序的运行安全。

什么是Spring

spring 提供 ioc 技术,容器会帮你管理依赖的对象,从而不需要自己创建和管理依赖对象了,更轻松的实现了程序的解耦。

spring 提供了事务支持,使得事务操作变的更加方便。

spring 提供了面向切片编程,这样可以更方便的处理某一类的问题。

更方便的框架集成,spring 可以很方便的集成其他框架,比如 MyBatis、hibernate 等。

什么是 aop

aop 是面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。

简单来说就是统一处理某一“切面”(类)的问题的编程思想,比如统一处理日志、异常等。

什么是 ioc

ioc:Inversionof Control(中文:控制反转)是 spring 的核心,对于 spring 框架来说,就是由 spring 来负责控制对象的生命周期和对象间的关系。

简单来说,控制指的是当前对象对内部成员的控制权;控制反转指的是,这种控制权不由当前对象管理了,由其他(类,第三方容器)来管理。

spring mvc 运行流程

spring mvc 先将请求发送给 DispatcherServlet。

DispatcherServlet 查询一个或多个 HandlerMapping,找到处理请求的 Controller。

DispatcherServlet 再把请求提交到对应的 Controller。

Controller 进行业务逻辑处理后,会返回一个ModelAndView。

Dispathcher 查询一个或多个 ViewResolver 视图解析器,找到 ModelAndView 对象指定的视图对象。
视图对象负责渲染返回给客户端。

什么是 spring boot?

spring boot 是为 spring 服务的,是用来简化新 spring 应用的初始搭建以及开发过程的。

为什么要用 spring boot?

无代码生成和 xml 配置

提供应用监控

提升开发效率

8.Redis

随着QPS的逐渐升高,传统的mysql数据库已经无法满足。所以有了基于内存的redis缓存数据库来存储热点数据。

什么是Redis

Redis 是一个使用 C 语言开发的高速缓存数据库。

Redis 使用场景:

  • 记录帖子点赞数、点击数、评论数;

  • 缓存近期热帖;

  • 缓存文章详情信息;

  • 记录用户会话信息。

Redis 的功能

  • 数据缓存功能
  • 分布式锁的功能
  • 支持数据持久化
  • 支持消息队列

Redis 和 memcache

存储方式不同:memcache 把数据全部存在内存之中,断电后会挂掉,数据不能超过内存大小;Redis 有部份存在硬盘上,这样能保证数据的持久性。

数据支持类型:memcache 对数据类型支持相对简单;Redis 有复杂的数据类型。

使用底层模型不同:它们之间底层实现方式,以及与客户端之间通信的应用协议不一样,Redis 自己构建了 vm 机制,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求。

value 值大小不同:Redis 最大可以达到 1gb;memcache 只有 1mb。

Redis 为什么是单线程的

因为 cpu 不是 Redis 的瓶颈,Redis 的瓶颈最有可能是机器内存或者网络带宽。既然单线程容易实现,而且 cpu 又不会成为瓶颈,那就顺理成章地采用单线程的方案了。

关于 Redis 的性能,官方网站也有,普通笔记本轻松处理每秒几十万的请求。

而且单线程并不代表就慢 nginx 和 nodejs 也都是高性能单线程的代表。

缓存穿透:指查询一个一定不存在的数据,由于缓存是不命中时需要从数据库查询,查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到数据库去查询,造成缓存穿透。

解决方案:最简单粗暴的方法如果一个查询返回的数据为空(不管是数据不存在,还是系统故障),我们就把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。

Redis 数据类型

Redis 支持的数据类型:string(字符串)、list(列表)、hash(字典)、set(集合)、zset(有序集合)。

Redis 深度历险:核心原理与应用实践

《Redis 深度历险:核心原理与应用实践》分为基础和应用篇、原理篇、集群篇、拓展篇、源码篇共 5 大块内容。基础和应用篇讲解对读者来说最有价值的内容,可以直接应用到实际工作中;原理篇、集群篇让开发者透过简单的技术表面看到精致的底层世界;拓展篇帮助读者拓展技术视野和夯实基础,便于进阶学习;源码篇让高阶的读者能够读懂源码,掌握核心技术实力。

9.Zookeeper

Zookeeper作为统一配置文件管理和集群管理框架,是后续学习其他框架的基础,在微服务中,还可以用来做注册中心。

什么是zookeeper

zookeeper 是一个分布式的,开放源码的分布式应用程序协调服务,是 google chubby 的开源实现,是 hadoop 和 hbase 的重要组件。它是一个为分布式应用提供一致性服务的软件,提供的功能包括:配置维护、域名服务、分布式同步、组服务等。

zookeeper 的功能

集群管理:监控节点存活状态、运行请求等。

主节点选举:主节点挂掉了之后可以从备用的节点开始新一轮选主,主节点选举说的就是这个选举的过程,使用 zookeeper 可以协助完成这个过程。

分布式锁:zookeeper 提供两种锁:独占锁、共享锁。独占锁即一次只能有一个线程使用资源,共享锁是读锁共享,读写互斥,即可以有多线线程同时读同一个资源,如果要使用写锁也只能有一个线程使用。zookeeper可以对分布式锁进行控制。

命名服务:在分布式系统中,通过使用命名服务,客户端应用能够根据指定名字来获取资源或服务的地址,提供者等信息。

zookeeper 的部署模式

zookeeper 有三种部署模式:

单机部署:一台集群上运行;

集群部署:多台集群运行;

伪集群部署:一台集群启动多个 zookeeper 实例运行。

10.Kafka

kafka和zookeeper的关系

kafka 不能脱离 zookeeper 单独使用,因为 kafka 使用 zookeeper 管理和协调 kafka 的节点服务器。

kafka的数据保留的策略?

kafka 有两种数据保存策略:按照过期时间保留和按照存储的消息大小保留。

kafka 同时设置了 7 天和 10G 清除数据,到第五天的时候消息达到了 10G,这个时候 kafka 将如何处理?
这个时候 kafka 会执行数据清除工作,时间和大小不论那个满足条件,都会清空数据。

kafka性能瓶颈

cpu 性能瓶颈

磁盘读写瓶颈

kafka集群

集群的数量不是越多越好,最好不要超过 7 个,因为节点越多,消息复制需要的时间就越长,整个群组的吞吐量就越低。

集群数量最好是单数,因为超过一半故障集群就不能用了,设置为单数容错率更高。

Apache Kafka实战

《Apache Kafka实战》是一本涵盖Apache Kafka各方面的具有实践指导意义的工具书和参考书。作者结合典型的使用场景,对Kafka整个技术体系进行了较为全面的讲解,以便读者能够举一反三,直接应用于实践。同时,本书还对Kafka的设计原理及其流式处理组件进行了较深入的探讨,并给出了翔实的案例。

11.ES

elasticsearch简写es,es是一个高扩展、开源的全文检索和分析引擎,它可以准实时地快速存储、搜索、分析海量的数据。

12.Dubbo

Dubbo是一个分布式服务框架,致力于提供高性能和透明化的RPC远程服务调用方案,以及SOA服务治理方案。简单的说,dubbo就是个服务框架

13.SpringCloud

Spring Cloud是一个微服务框架。Spring Cloud提供了全套的分布式系统解决方案,不仅对微服务基础框架Netflix的多个开源组件进行了封装,同时还实现了和云端平台以及Spring Boot开发框架的集成。

什么是 spring cloud

spring cloud 是一系列框架的有序集合。它利用 spring boot 的开发便利性巧妙地简化了分布式系统基础设施的开发,如服务发现注册、配置中心、消息总线、负载均衡、断路器、数据监控等,都可以用 spring boot 的开发风格做到一键启动和部署。

spring cloud 的核心组件

Eureka:服务注册于发现。

Feign:基于动态代理机制,根据注解和选择的机器,拼接请求 url 地址,发起请求。

Ribbon:实现负载均衡,从一个服务的多台机器中选择一台。

Hystrix:提供线程池,不同的服务走不同的线程池,实现了不同服务调用的隔离,避免了服务雪崩的问题。

Zuul:网关管理,由 Zuul 网关转发请求给对应的服务。

断路器的作用

在分布式架构中,断路器模式的作用也是类似的,当某个服务单元发生故障(类似用电器发生短路)之后,通过断路器的故障监控(类似熔断保险丝),向调用方返回一个错误响应,而不是长时间的等待。这样就不会使得线程因调用故障服务被长时间占用不释放,避免了故障在分布式系统中的蔓延。

14.Nginx

Nginx是一个高性能的HTTP和反向代理服务器。具有占内存少和并发能力强的特点。

15.Netty

netty 是一个基于nio的客户、服务器端编程框架,netty提供异步的,事件驱动的网络应用程序框架和工具,可以快速开发高可用的客户端和服务器。

16.架构设计

好的架构从来不是设计出来的,而是演变出来的。所以我们在日常开发中就要不断思考性能的优化。

下面拿如何设计一个百万人抽奖系统举例说明架构的演进。

V0——单体架构

如果现在让你实现几十人的抽奖系统,简单死了吧,直接重拳出击!

两猫一豚走江湖,中奖入库,调通知服务,查库通知,完美!

相信大家学java时可能都做过这种案例,思考🤔一下存在什么问题?

  • 单体服务,一着不慎满盘皆输
  • 抽了再抽,一个人就是一支军队
  • 恶意脚本,没有程序员中不了的奖

接下来就聊聊怎么解决这些问题?

V1——负载均衡

当一台服务器的单位时间内的访问量越大时,服务器压力就越大,大到超过自身承受能力时,服务器就会崩溃。

为了避免服务器崩溃,让用户有更好的体验,我们通过负载均衡的方式来分担服务器压力。

负载均衡就是建立很多很多服务器,组成一个服务器集群,当用户访问网站时,先访问一个中间服务器,好比管家,由他在服务器集群中选择一个压力较小的服务器,然后将该访问请求引入该服务器。

如此以来,用户的每次访问,都会保证服务器集群中的每个服务器压力趋于平衡,分担了服务器压力,避免了服务器崩溃的情况。

负载均衡是用「反向代理」的原理实现的。具体负载均衡算法及其实现方式我们下文再续。

负载均衡虽然解决了单体架构一着不慎满盘皆输的问题,但服务器成本依然不能保护系统周全,我们必须想好一旦服务器宕机,如何保证用户的体验。

即如何缓解开奖一瞬间时的大量请求。

V2——服务限流

限流主要的作用是保护服务节点或者集群后面的数据节点,防止瞬时流量过大使服务和数据崩溃(如前端缓存大量实效),造成不可用。

还可用于平滑请求。

在上一小节我们做好了负载均衡来保证集群的可用性,但公司需要需要考虑服务器的成本,不可能无限制的增加服务器数量,一般会经过计算保证日常的使用没问题。

限流的意义就在于我们无法预测未知流量,比如刚提到的抽奖可能遇到的:

其他一些场景:

  • 热点事件(微博)

这些情况都是无法预知的,不知道什么时候会有10倍甚至20倍的流量打进来,如果真碰上这种情况,扩容是根本来不及的(弹性扩容都是虚谈,一秒钟你给我扩一下试试)

明确了限流的意义,我们再来看看如何实现限流

防止用户重复抽奖

重复抽奖和恶意脚本可以归在一起,同时几十万的用户可能发出几百万的请求。

如果同一个用户在1分钟之内多次发送请求来进行抽奖,就认为是恶意重复抽奖或者是脚本在刷奖,这种流量是不应该再继续往下请求的,在负载均衡层给直接屏蔽掉。

可以通过nginx配置ip的访问频率,或者在在网关层结合sentinel配置限流策略。

用户的抽奖状态可以通过redis来存储,后面会说。

拦截无效流量

无论是抽奖还是秒杀,奖品和商品都是有限的,所以后面涌入的大量请求其实都是无用的。

举个例子,假设50万人抽奖,就准备了100台手机,那么50万请求瞬间涌入,其实前500个请求就把手机抢完了,后续的几十万请求就没必要让他再执行业务逻辑,直接暴力拦截返回抽奖结束就可以了。

同时前端在按钮置灰上也可以做一些文章。

那么思考一下如何才能知道奖品抽完了呢,也就是库存和订单之前的数据同步问题。

服务降级和服务熔断

有了以上措施就万无一失了吗,不可能的。所以再服务端还有降级和熔断机制。

在此简单做个补充,详细内容请持续关注作者。

有好多人容易混淆这两个概念,通过一个小例子让大家明白:

假设现在一条粉丝数突破100万,冲上微博热搜,粉丝甲和粉丝乙都打开微博观看,但甲看到了一条新闻发布会的内容,乙却看到”系统繁忙“,过了一会,乙也能看到内容了。

(请允许一条幻想一下😎)

在上述过程中,首先是热点时间造成大量请求,发生了服务熔断,为了保证整个系统可用,牺牲了部分用户乙,乙看到的”系统繁忙“就是服务降级(fallback),过了一会有恢复访问,这也是熔断器的一个特性(hystrix)

V3 同步状态

接着回到上一节的问题,如何同步抽奖状态?

这不得不提到redis,被广泛用于高并发系统的缓存数据库。

我们可以基于Redis来实现这种共享抽奖状态,它非常轻量级,很适合两个层次的系统的共享访问。

当然其实用ZooKeeper也是可以的,在负载均衡层可以基于zk客户端监听某个znode节点状态。一旦抽奖结束,抽奖服务更新zk状态,负载均衡层会感知到。

V4线程优化

对于线上环境,工作线程数量是一个至关重要的参数,需要根据自己的情况调节。

众所周知,对于进入Tomcat的每个请求,其实都会交给一个独立的工作线程来进行处理,那么Tomcat有多少线程,就决定了并发请求处理的能力。

但是这个线程数量是需要经过压测来进行判断的,因为每个线程都会处理一个请求,这个请求又需要访问数据库之类的外部系统,所以不是每个系统的参数都可以一样的,需要自己对系统进行压测。

但是给一个经验值的话,Tomcat的线程数量不宜过多。因为线程过多,普通服务器的CPU是扛不住的,反而会导致机器CPU负载过高,最终崩溃。

同时,Tomcat的线程数量也不宜太少,因为如果就100个线程,那么会导致无法充分利用Tomcat的线程资源和机器的CPU资源。

所以一般来说,Tomcat线程数量在200~500之间都是可以的,但是具体多少需要自己压测一下,不断的调节参数,看具体的CPU负载以及线程执行请求的一个效率。

在CPU负载尚可,以及请求执行性能正常的情况下,尽可能提高一些线程数量。

但是如果到一个临界值,发现机器负载过高,而且线程处理请求的速度开始下降,说明这台机扛不住这么多线程并发执行处理请求了,此时就不能继续上调线程数量了。

V5业务逻辑

抽奖逻辑怎么做?

好了,现在该研究一下怎么做抽奖了

在负载均衡那个层面,已经把比如50万流量中的48万都拦截掉了,但是可能还是会有2万流量进入抽奖服务。

因为抽奖活动都是临时服务,可以阿里云租一堆机器,也不是很贵,tomcat优化完了,服务器的问题也解决了,还剩啥呢?

Mysql,是的,你的Mysql能抗住2万的并发请求吗?

答案是很难,怎么办呢?

把Mysql给替换成redis,单机抗2万并发那是很轻松的一件事情。

而且redis的一种数据结构set很适合做抽奖,可以随机选择一个元素并剔除。

V6流量削峰

由上至下,还剩中奖通知部分没有优化。

思考这个问题:假设抽奖服务在2万请求中有1万请求抽中了奖品,那么势必会造成抽奖服务对礼品服务调用1万次。

那也要和抽奖服务同样处理吗?

其实并不用,因为发送通知不要求及时性,完全可以让一万个请求慢慢发送,这时就要用到消息中间件,进行限流削峰。

也就是说,抽奖服务把中奖信息发送到MQ,然后通知服务慢慢的从MQ中消费中奖消息,最终完成完礼品的发放,这也是我们会延迟一些收到中奖信息或者物流信息的原因。

假设两个通知服务实例每秒可以完成100个通知的发送,那么1万条消息也就是延迟100秒发放完毕罢了。

同样对MySQL的压力也会降低,那么数据库层面也是可以抗住的。

看一下最终结构图:

17.Linux

作为Java程序员,不会用Linux会让人笑掉大牙的。我们不必像运维兄弟一样精通,基本的命令还是要熟练掌握。

  • 理解一切皆文件
  • 文件操作命令
  • 权限管理命令
  • 系统磁盘命令

18.Git

网上经常传出不会git被开除的新闻,所以还不学起来?

200条Git命令大全

19.数据结构和算法

问一下各位什么是程序?——数据结构+算法。所以,学吧,刷题吧

《八大排序》源码](https://pan.baidu.com/s/1woTgwkVUT1xtgMB1ha36Uw),提取码:5ehp

image-20210923184932295.png

准备

古语云:“兵马未动,粮草先行”。想跟着一条一块把「排序算法」弄明白的,建议先准备好以下代码模板。

📢 观看本教程需知道基本循环语法两数交换双指针等前置知识。

📚 建议先看完代码逐步分析后再尝试自己写。

  • 新建一个Java工程,本文全篇也基于Java语言实现代码。
  • 建立如下目录结构
  • MainTest测试类中编写测试模板。
/**
 * 测试类
 * Author:一条
 * Date:2021/09/23
 */
public class MainTest {
    public static void main(String[] args) {
        //待排序序列
        int[] array={6,10,4,5,2,8};
        //调用不同排序算法
				// BubbleSort.sort(array);

        // 创建有100000个随机数据的数组
        int[] costArray=new int[100000];
        for (int i = 0; i < 100000; i++) {
            // 生成一个[0,100000) 的一个数
            costArray[i] = (int) (Math.random() * 100000);
        }

        Date start = new Date();
        //过长,先注释掉逐步打印
				//BubbleSort.sort(costArray);
        Date end = new Date();
        System.out.println("耗时:"+(end.getTime()-start.getTime())/1000+"s");
    }
}

该段代码内容主要有两个功能:

  • 调用不同的排序算法进行测试
  • 测试不同排序算法将10w个数排好序需要的时间。更加具象的理解时间复杂度的不同

1.冒泡排序

通过对乱序序列从前向后遍历,依次比较相邻元素的值,若发现逆序则交换,使值较大的元素逐渐从前移向后部。

像水底下的气泡一样逐渐向上冒一样。

不理解的小伙伴可以用debug模式逐步分析。

/**
 * 冒泡排序
 * Author:一条
 * Date:2021/09/23
 */
public class BubbleSort{
    public static int[] sort(int[] array){
        for (int i = 0; i < array.length; i++) {
            for (int j = 0; j < array.length-1; j++) {
              //依次比较,将最大的元素交换到最后
                if (array[j]>array[j+1]){
                  // 用临时变量temp交换两个值
                    int temp=array[j];
                    array[j]=array[j+1];
                    array[j+1]=temp;
                }
            }
          //输出每一步的排序结果
            System.out.println(Arrays.toString(array));
        }
        return array;
    }
}

输出结果

逐步分析

  1. 初始数组:[6,10,4,5,2,8]
  2. 6拿出来和后一个10比较,6<10,不用交换。- > j++;
  3. 10拿出来和后一个4比较,10>4,交换。- > [6,4,10,5,2,8]
  4. 依次执行j++与后一个比较交换
  5. 第一层i循环完,打印第一行- > [6, 4, 5, 2, 8, 10],此时最后一位10在正确位置上。 - > i++
  6. 4开始,继续比较交换,倒数第二位8回到正确位置。
  7. 如上循环下去 - > ……
  8. 最终结果 - > [2, 4, 5, 6, 8, 10]

这时再回去看动图理解。

记得先注释掉排序类逐步打印代码。

时间复杂度O(n^2)

算法优化

优化点一

外层第一次遍历完,最后一位已经是正确的,j就不需要再比较,所以结束条件应改为j-i-1;

优化点二

因为排序的过程中,各元素不断接近自己的位置,如果一趟比较下来没有进行过交换,就说明序列有序,因此要在排序过程中设置一个标志flag判断元素是否进行过交换。从而减少不必要的比较。

优化代码

public static int[] sortPlus(int[] array){
        System.out.println("优化冒泡排序开始----------");
        for (int i = 0; i < array.length; i++) {
            boolean flag=false;
            for (int j = 0; j < array.length-i-1; j++) {
                if (array[j]>array[j+1]){
                    flag=true;
                    int temp=array[j];
                    array[j]=array[j+1];
                    array[j+1]=temp;
                }
            }
            if (flag==false){
                break;
            }
//            System.out.println(Arrays.toString(array));
        }
        return array;
    }

优化测试

通过基础测试看到当序列已经排好序,即不发生交换后终止循环。

耗时测试由27s优化到17s

2.选择排序

选择排序和冒泡排序很像,是从乱序序列的数据中,按指定的规则选出某一元素,再依规定交换位置后达到排序的目的。

public class SelectSort {
    public static int[] sort(int[] array) {
        System.out.println("选择排序开始----------");
        for (int i = 0; i < array.length; i++) {
          //每个值只需与他后面的值进行比较,所以从开始
            for (int j = i; j < array.length; j++) {
              //注意此处是哪两个值比较
                if (array[i]>array[j]){
                    int temp=array[i];
                    array[i]=array[j];
                    array[j]=temp;
                }
            }
            System.out.println(Arrays.toString(array));
        }
        return array;
    }
}

输出结果

逐步分析

  • 初始数组:[6,10,4,5,2,8]
  • 拿出610比较,不交换 - > j++
  • 62比较,交换 - > j++
  • 注意此时是拿2继续比较,都不交换,确定第一位(最小的数)为2 - > i++
  • 循环下去,依次找到第一小,第二小,……的数
  • 最终结果 - > [2, 4, 5, 6, 8, 10]

这时再回去看动图理解。

时间复杂度:O(n^2)

上诉代码中使用交换的方式找到较小值,还可以通过移动的方式,即全部比较完只交换一次。

这种对空间的占有率会有些增益,但对时间的增益几乎没有,可忽略,亦不再演示。

3.插入排序

把n个乱序的元素看成为一个有序表和一个无序表,开始时有序表中只包含一个元素,无序表中包含有n-1个元素,排序过程中通过不断往有序表插入元素,获取一个局部正确解,逐渐扩大有序序列的长度,直到完成排序。

2021-09-25 19.20.05

/**
 * 插入排序
 * Author:一条
 * Date:2021/09/23
 */
public class InsertSort {
    public static void sort(int[] array) {
        for (int i = 1; i < array.length; i++) {
            //插入有序序列,且将有序序列扩大
            for (int j = i; j > 0; j--) {
                if (array[j]>array[j-1]){
                    int temp=array[j];
                    array[j]=array[j-1];
                    array[j-1]=temp;
                }
            }
//            System.out.println(Arrays.toString(array));
        }
    }
}

输出结果

见下方希尔排序,就是希尔对插入排序的优化。

4.希尔排序

希尔排序是插入排序的一个优化,思考往[2,3,4,5,6]中插入1,需要将所有元素的位置都移动一遍,也就是说在某些极端情况下效率不高,也称该算法不稳定

希尔排序是插入排序经过改进之后的一个更高效的版本,也称为缩小增量排序

希尔排序是把记录按下标的一定增量分组,对每组使用插入排序;

随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个序列恰被分成一组,算法便终止。

和插入排序一样,从局部到全部,希尔排序是局部再局部。

图片源于网络

/**
 * 希尔排序
 * Author:一条
 * Date:2021/09/23
 */
public class ShellSort {
    public static void sort(int[] array) {
        System.out.println("希尔排序开始--------");
        //gap初始增量=length/2  逐渐缩小:gap/2
        for (int gap = array.length/2; gap > 0 ; gap/=2) {
            //插入排序 交换法
            for (int i = gap; i < array.length ; i++) {
                int j = i;
                while(j-gap>=0 && array[j]<array[j-gap]){
                    //插入排序采用交换法
                    int temp = array[j];
                    array[j]=array[j-gap];
                    array[j-gap]=temp;
                    j-=gap;
                }
            }
            System.out.println(Arrays.toString(array));
        }
    }
}

输出结果

5.快速排序

快速排序(Quicksort)是对冒泡排序的一种改进,相比冒泡排序,每次的交换都是跳跃式的。

将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。

体现出分治的思想。

思路如下:

  • 首先在这个序列中找一个数作为基准数,为了方便可以取第一个数。
  • 遍历数组,将小于基准数的放置于基准数左边,大于基准数的放置于基准数右边。此处可用双指针实现。
  • 此时基准值把数组分为了两半,基准值算是已归位(找到排序后的位置)
  • 利用递归算法,对分治后的子数组进行排序。
public class QuickSort {
    public static void sort(int[] array) {
        System.out.println("快速排序开始---------");
        mainSort(array, 0, array.length - 1);
    }

    private static void mainSort(int[] array, int left, int right) {
        if(left > right) {
            return;
        }
        //双指针
        int i=left;
        int j=right;
        //base就是基准数
        int base = array[left];
        //左边小于基准,右边大于基准
        while (i<j) {
            //先看右边,依次往左递减
            while (base<=array[j]&&i<j) {
                j--;
            }
            //再看左边,依次往右递增
            while (base>=array[i]&&i<j) {
                i++;
            }
            //交换
            int temp = array[j];
            array[j] = array[i];
            array[i] = temp;
        }
        //最后将基准为与i和j相等位置的数字交换
        array[left] = array[i];
        array[i] = base;
        System.out.println(Arrays.toString(array));
        //递归调用左半数组
        mainSort(array, left, j-1);
        //递归调用右半数组
        mainSort(array, j+1, right);
    }
}

输出结果

逐步分析

  • 6作为基准数,利用左右指针使左边的数<6,右边的数>6
  • 对左右两边递归,即左边用5作为基准数继续比较。
  • 直到left > right结束递归。

优化一

三数取中(median-of-three):我们目前是拿第一个数作为基准数,对于部分有序序列,会浪费循环,可以用三数取中法优化,感性的小伙伴可自行了解。

优化二

快速排序对于长序列非常快,但对于短序列不如插入排序。可以综合使用。

完整文章

leetcode刷题

最长回文子串——滑动窗口

回文的意思是正着念和倒着念一样,如:上海自来水来自海上

——leetcode此题热评

在对联中就有回文的手法,“上海自来水来自海上”,大家有下联了吗

image-20210802133512345

哈喽,大家好,我是一条。

糊涂算法,难得糊涂

今天来一道中等题,看看自己功力几何?

Question

5. 最长回文子串

难度:中等

给你一个字符串 s,找到 s 中最长的回文子串。

输入:s = "babad"
输出:"bab"
解释:"aba" 同样是符合题意的答案。
输入:s = "cbbd"
输出:"bb"
输入:s = "a"
输出:"a"
输入:s = "ac"
输出:"a"

1 <= s.length <= 1000
s 仅由数字和英文字母(大写和/或小写)组成

Solution

之前我们做过一道题是:最长不重复子串。当时使用的是滑动窗口法

这道题增加了回文的约束,那么该怎么处理回文,可以在纸上写一个回文串观察一下,有什么特点呢?

从中心点向两端发散

对吧,所以我们要滑动这个中心点,并对每一个中心点,进行左右扩展,判断左右字符是否相等即可。

  • null和空字符串的特殊情况处理掉
  • 有一个可滑动且大小可变的窗口,窗口左端(start)不动,右端(end)向后移动
  • 由于字符串长度有奇数和偶数两种,所以我们需要同时计算从一个字符开始扩展和从两个字符之间开始扩展
  • 找出两种扩展中相对长的作为当前最大值
  • 新的start要从何处开始,如何扩展字符串只需要考虑的问题

还有一种马拉车算法更加快速,但有一定难度,面试一般不会出现,感兴趣的同学可以看一下

所有leetcode代码已同步至github

欢迎star

/**
 * @author yitiaoIT
 */
class Solution {
    public String longestPalindrome(String s) {
    if (s == null || s.length() < 1) return "";
    int start = 0, end = 0;
    for (int i = 0; i < s.length(); i++) {
        int len1 = expandAroundCenter(s, i, i);
        int len2 = expandAroundCenter(s, i, i + 1);
        int len = Math.max(len1, len2);
        if (len > end - start) {
            start = i - (len - 1) / 2;
            end = i + len / 2;
        }
    }
    return s.substring(start, end + 1);
}

private int expandAroundCenter(String s, int left, int right) {
    int L = left, R = right;
    while (L >= 0 && R < s.length() && s.charAt(L) == s.charAt(R)) {
        L--;
        R++;
    }
    return R - L - 1;
}

Result

复杂度分析

  • 时间复杂度:O(N)

image-20210802132533270

20.计算机网络

计算机基础课,非科班学生提升必备。

21.操作系统

计算机基础课,非科班学生提升必备。

22.计算机组成原理

计算机基础课,非科班学生提升必备。

23.程序员英语

因为经常会看一些英文的文档,四级水平时一定要达到的。

24.写作能力

即使不做自媒体,写文档的能力也要培养起来。

25.演讲能力

千万不要小瞧了这个能力,升职答辩,年终述职必备。

26.管理能力

不可能搞一辈子技术的,总要带团队,走管理路线。

1.阿里巴巴Java开发手册

《阿里巴巴Java开发手册》是阿里内部Java工程师所遵循的开发规范,涵盖编程规约、单元测试规约、异常日志规约、MySQL规约、工程规约、安全规约等,这是近万名阿里Java技术精英的经验总结,并经历了多次大规模一线实战检验及完善。这是阿里回馈给Java社区的一份礼物,希望能够帮助企业开发团队在Java开发上更高效、容错、有协作性,提高代码质量,降低项目维护成本。

2.程序员一条

3.Java核心知识点

4.204道全路线面试题合集

204道全路线面试题合集


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK