ZB-013-java多态

什么是多态

  • 实例方法默认是多态的
    • 在运行时根据this来决定调用那个方法
    • 静态方法没有多态
    • 参数静态绑定,接受者动态绑定
      1. 多态只对方法的接受者生效
      2. 多态只选择接受者的类型,不选择参数的类型
  • 例子:多态
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class Main {
public static void main(String[] args) {
// 多态 obj调用print方法 会走进 Sub
Base obj = new Sub();
// 多态 子类对象 SubParam的实例 可以 赋值给 父类引用
BaseParam baseParam = new SubParam();

obj.print(baseParam); // 由于是子类对象,所以会调用 子类的 print方法
// 参数的类型 存在 父子关系,是否会发生多态调用?
// 不会 , 参数是 类型最匹配优先,所以 会按"声明"的时候的定义传递进去
}
}

class Base {
public void print(BaseParam baseParam){
System.out.println("I'm base, print baseParam");
}
public void print(SubParam subParam){
System.out.println("I'm base, print subParam");
}
}

class Sub extends Base{
@Override
public void print(BaseParam baseParam){
System.out.println("I'm sub, print baseParam");
}
@Override
public void print(SubParam subParam){
System.out.println("I'm sub, print subParam");
}
}
class BaseParam {}
class SubParam extends BaseParam{}
  • 例子
    1. shape
    2. HashSet.addAll()
      1
      2
      3
      4
      5
      1 HashSet自己没有 addAll ,从父类继承来的,
      2 调用addAll 会调用 add 方法
      3 而 HashSet 和 父类 都有 add 方法,那么会调用谁的呢?
      4 默念口诀: 实例方法默认是多态的,多态的意思是根据当前的类型来决定调用那个方法
      5 虽然 addAll 在父类中 但当前对象是 HashSet 的实例。所有会调用 HashSet 的 add

相关例子

  • https://github.com/hcsp/shape-polymorphism
  • https://github.com/hcsp/polymorphism-overload-method-selection
  • 策略模式,打折策略
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    public static int calculatePrice(String discountStrategy, int price, User user) {
    switch (discountStrategy) {
    case "NoDiscount":
    return price;
    case "Discount95":
    return (int) (price * 0.95);
    case "OnlyVip":
    {
    if (user.isVip()) {
    return (int) (price * 0.95);
    } else {
    return price;
    }
    }
    default:
    throw new IllegalStateException("Should not be here!");
    }
    }

    // 这样的缺点是 后来又多了 N种策略怎么办,别人都不敢轻易改代码了
    把每个策略抽象成一种类型单独维护
    public static int calculatePrice(DiscountStrategy strategy, int price, User user) {
    return strategy.discount(price, user);
    }

    // DiscountStrategy 是父类
    public class DiscountStrategy {
    public int discount(int price, User user) {
    throw new UnsupportedOperationException();
    }
    }
    // 使用 多态实现不同策略
    public class Discount95Strategy extends DiscountStrategy{
    @Override
    public int discount(int price, User user) {
    return (int)(price*0.95);
    }
    }

    public class NoDiscountStrategy extends DiscountStrategy{
    @Override
    public int discount(int price, User user) {
    return price;
    }
    }

    public class OnlyVipDiscountStrategy extends DiscountStrategy{
    @Override
    public int discount(int price, User user) {
    if(user.isVip()){
    return (int)(price*0.95);
    }else{
    return price;
    }
    }
    }
    // 这样每次增加策略就新建一个类就行了。

    // 这是个优点,也是个缺点。是因为 如果10000个策略就10000个类

    // 这样运用“多态”后,“业务和策略”就分离了

涉及金额绝对不能用double

  1. 所有金额乘以 100 (1元= 100分),int去处理
  2. BigDecimal类型 任意精度10进制数

ThreadPoolExecutor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
TheadPoolExecutor(
int corePoolSize,
int maxiumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockKingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler){
...
}


)

// 最后一个参数的意思
RejectedExecutionHandler 就是策略模式在线程池中的应用

做菜实例

ZB-012-java组合和继承

什么是继承

  • 程序员的宿命:复制 & 粘贴
    • 真的只能这样吗?
    • DRY(Donot Repeat Youself)
    • 事不过三,三则重构
  • 继承的本质是提炼代码,避免重复
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class Cat {
private String name;
public void setName(String name) {
this.name = name;
}
public void walk(){}
}

public class Dog {
private String name;
public void setName(String name) {
this.name = name;
}
public void walk(){}
}

public class Pig {
private String name;
public void setName(String name) {
this.name = name;
}
public void walk(){}
}

// 它们又很多共性和重复
// 一旦walk改变了,要改三个地方

继承,避免重复

1
2
3
4
5
6
7
8
public class Animal{
private String name;
public void walk(){}
}

public class Cat extends Amimal{...}
public class Dog extends Amimal{...}
public class Pig extends Amimal{...}

Java的继承体系

  • 单继承
  • Object 是所有类的基类,所有对象都继承了Object,所以所有对象都有 Object 的非私有成员属性/方法

    • equals

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      Object 的 equals 默认实现是 
      public boolean equals(Object obj){
      return this == obj; // 比较的是 addr
      }

      针对其他类 如 Order 根据 id就可以确定是不是同一订单
      public class Order {
      private int id;
      private String name;

      @Override
      public boolean equals(Object o) {
      if (this == o) return true;
      if (o == null || getClass() != o.getClass()) return false;
      Cat cat = (Cat) o;
      return id == cat.id;
      }

      @Override
      public int hashCode() {
      return Objects.hash(id);
      }
      }
    • toString

      1
      2
      3
      4
      5
      6
      7
      8
      Object 的 toString 默认实现是 
      public boolean toString(){
      return getClass().getName() + "@" + Integer.toHexString(hashCode()) ;
      }

      任何时候打印对象的时候 都会提供字符串表示
      System.out.println(xxx); // 隐式调用
      xxx.toString();

单根继承的好处是:

  • 所有对象都有某一行为,因为都继承了 Object
  • 方便处理

如 python支持多继承。会带来很多问题——菱形继承

  • 父类都有 xxx()函数的处理!!!不知道该调用谁,但是python解决了!不再详细解释

equals 约定

  • 自反性,任何非空的 x ,x.equals(x) 应该返回true
  • 对称性,对于任意非空的x、y ,如果x.equals(y) 返回true,那么 y.equals(x)也该返回 true
  • 传递性,对于任意非空的x、y、z,如果x.equals(y) ,y.equals(z)返回truex.equals(z)也该返回 true
  • 一致性,对于任意非空的x、y, 任何时候 x.equals(y) 都应该返回 true 或者 false,不能一会返回true、一会返回false
  • 非空性,对于不等于null的x,x.equals(null)必须返回false。要遵守这个约定,只需要在equals里面加上这么一段代码 if(o == null) return false,然而,这样做往往是没有必要的,因为我们都会在equals方法的第一步,做instanceof校验,检查参数是否为正确的类型。而a.instanceOf(null)会返回false。

每当你想覆盖 equals方法的时候,都会同时帮你生成一个 hashCode

  • java约定:当你覆盖 equals的时候 就要同时覆盖hashCode
  • equals 判断的是 两个对象里的内容是不是相等的
  • == 判断的是 两个对象 是不是相同的 addr一致

类的结构和初始化顺序

  • 子类拥有父类一切的数据和行为
  • 父类先于子类
  • 必须拥有匹配的构造器
    • super
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Animal {
String name;
{
System.out.println("初始化父类块");
}
public Animal(String name) {
this.name = name;
}
}


public class Cat extends Animal{
int age;
{
System.out.println("初始化子类块");
}
public Cat(String name,int age) {
super(name);
this.age = age;
}
}
  • 过程就是自顶向下依次初始化
    • step01 Animal 成员初始化
    • step02 Animal 成员初始化块
    • step03 Animal 构造器
    • step04 Cat 成员初始化
    • step05 Cat 成员初始化块
    • step06 Cat 构造器

实例方法的覆盖

  • 又称为重写/覆盖
  • 永远使用@Override注解来防止手残
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    public class Animal {
    String name;

    public Animal(String name) {
    this.name = name;
    }

    public void sayHello(){
    System.out.println("hello");
    }
    }

    // ----------------------------------
    public class Cat extends Animal{
    int age;

    public Cat(String name,int age) {
    super(name);
    this.age = age;
    }

    @Override
    public void sayHello(){
    System.out.println("cat hello");
    }
    }

模版方法模式

  • spring的初始化代码就是模版方法,定义了一些预定义好的方法。自定义的实现可以覆盖模版的方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 模版方法
public class BookWriter {
// 写书
public void wtiteBook(){
writeTitle();
writeContent();
writeEnding();
}
public void writeTitle(){
System.out.println("标题");
}
public void writeContent(){
System.out.println("内容");
}
public void writeEnding(){
System.out.println("结尾");
}

}


// 子类继承父类的模版方法
public class MyBookWriter extends BookWriter {
@Override
public void writeContent(){
System.out.println("我的内容");
}

public static void main(String[] args) {
new MyBookWriter().wtiteBook();
}
}

向上和向下转型

  • 一个子类对象一定是父类类型的对象

    • 正如一只猫同时也是一个动物,同时也是一个对象
    • instanceof 判断一个对象是不是一个类的实例

      1
      2
      3
      4
      5
      Object a = new Integer(1234);
      System.out.println(a instanceof Integer); // true
      System.out.println(a instanceof Number); // true
      System.out.println(a instanceof Object); // true
      System.out.println(null instanceof Integer); // false
    • null installceof ? 都是 false

  • 当需要一个父类型时,可以传递一个子类型

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    public static void main(String[] args) {
    setList(new ArrayList<Integer>());
    }

    public static void setList(AbstractList<Integer> list){
    }

    // <Integer> 是泛型, ArrayList 是 AbstractList 的子类

    // 还可以这样
    public static void main(String[] args) {
    setList(new ArrayList<Integer>());
    }

    public static void setList(AbstractList<? super Number> list){
    }
  • 但是,有的时候你必须进行一些转型

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
     public static void main(String[] args) {
    Animal a = new Animal();
    setAnimalName((Dog)animal);
    }

    public static void setAnimalName(Dog dog){
    }

    // 报错
    Exception in thread "main" java.lang.ClassCastException: Animal cannot be cast to Dog
    • 转型是不安全的
    • 失败了怎么办?

final

  • final声明变量是不可变的(必须初始化)

    1
    2
    final int a = 1; // 正确
    final int a ; // 错误 没有初始化
    • 局部变量/方法参数

      1
      2
      3
      4
      public void f(final Date date){

      }
      // date的地址不能改变, 地址指向的东西可以改变
    • 成员变量

    • 常量和单例

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      // 命名规则是全部大写 ,然后以下划线区分 如 MAX_VALUE
      public static final double PI = 3.1415926;

      // 单例模式
      public class World{
      private static final World SINGLETON_INSTANCE = new World();
      private World(){}

      public static World getInstance(){
      return SINGLETON_INSTANCE;
      }
      }
  • final声明: 禁止继承(在class上)/覆盖(方法)/重写此方法(方法)

  • final在类上声明: 禁止继承此类
    • 继承提供了灵活性,也埋下了隐患
    • 为什么String/Integer 等是 final的呢?
      1. 假设 MyInteger extends Integer 此时重写 compare 本来 1<2 而你偏偏 改写成 1>2 这样就破坏所有使用Integer方法 的约定
      2. JDK通过把 String/Integer 定义为 final 来阻止恶意的客户端继承常见类,破坏程序的约定

final在 类/方法上有什么优点

  1. 使用 final 可以保证类无法被继承,方法无法被重写,别人无法通过继承来破坏约定,你可以大胆的做一些事情,软件设计的一个约定——放心大胆的做一些事,我可以保证这个约定不会被打破
  2. 这个方法是 final 时方法是被确定的无法多态

组合和继承

  • 继承:is-a
  • 组合:has-a
    • 代码复用
    • 推荐使用组合,因为非常灵活

… 暂存 组合和继承

effective java

  • 一个类设计的时候就该确定,是否可以继承,还是final的(不可以继承的)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class CountingSet extends HashSet<Object> {
/** 统计"有史以来"向该集合中添加过的元素个数 */
private int count = 0;

@Override
public boolean add(Object obj) {
count++;
return super.add(obj);
}

@Override
public boolean addAll(Collection c) {
count += c.size();
return super.addAll(c);
}

public int getCount() {
return count;
}

// 我们希望创建一个Set,能够统计"有史以来"添加到其中去的元素个数
// 但是,现在结果明显不对
// 请尝试修复此问题
public static void main(String[] args) {
CountingSet countingSet = new CountingSet();
countingSet.add(new Object());
countingSet.addAll(Arrays.asList(1, 2, 3));

System.out.println(countingSet.getCount());
}
}
  • // 正确姿势是注释addAll里的 count += c.size()本行

    为啥 ?

  • 因为 我们的 类继承了 HashSet

  • HashSet 的 addAll 里会默认调用 add,所以结果是double
  • 这就是 “组合/继承” 使用的例子,因为 我们的 CountingSet 过分依赖了 父类的实现
  • 正确姿势
1
2
3
4
5
public class CountingSet{
// 组合的方式: 内部有一个 容器
HashSet<Object> innerSet;
// 用内部的 innerSet 去统计个数,而不是继承
}

ZB-011-java封装和访问控制

什么是封装

  • 隐藏内部细节,只暴露出接口
  • 电灯
    • 你只关心它的“开关”接口,不关心内部的“电路”细节
  • 汽车
    • 你只关心“方向盘”,不关心内部的细节

Light.java

1
2
3
4
5
6
7
8
9
10
11
12
public class Light{

public void trunOn(){
打开电路1();
打开电路2();
打开电路3();
}

public void 打开电路1(){}
public void 打开电路2(){}
public void 打开电路3(){}
}

Home.java

打开灯有两种方式

  • 直接调用 trunOn() (低耦合)
  • 调用实现细节 打开电路1();打开电路2();打开电路3()(高耦合)
1
2
3
4
5
6
7
8
9
10
11
class public Home{
public static void main(String[] args){
Light a = new Light();
a.trunOn();

Light b = new Light();
b.打开电路1();
b.打开电路2();
b.打开电路3();
}
}

如果有一天,一个高级工程师对打开灯的方式进行了优化

此时只要打开电路1();打开电路2(); 就可以开灯了

此时 以第二种方式调用开灯的人就要修改 因为它耦合了开灯的细节

一方改变另一方也要改变

而第一种方式只需要更改 turnOn() 自己一个方法就做到了正常工作

1
2
3
4
public void trunOn(){
打开电路1();
打开电路2();
}

场景二

Person类

1
2
3
4
5
public class Person{
int id;
int age;
String name;
}

假设10个人都用了你的 Person

1
2
3
Person p = new Person();
p.age = 10;
p.name = "张三";

老板突然提了个需求,如果年龄小与0则为0,大于100则为100

  • 这样别人都是通过 p.age = 10设置一个值。这样要改 10个地方。

优化

  • 把成员变量变为私有
  • 设置对应的 get/set 接口
  • 外界只能通过 get/set接口对成员进行操作
    1
    2
    3
    Person p = new Person();
    p.setAge(10);
    p.setName("张三");
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class Person {
private Integer id;
private String name;
// 如果年龄小与0则为0,大于100则为100
private int age;

public Integer getId() {
return id;
}

public void setId(Integer id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getAge() {
return age;
}

public void setAge(int age) {
if(age<0){
this.age = 0;
}else if(age>100){
this.age = 100;
}else{
this.age = age;
}
}
}

有没有发现改一下接口 setAge() 就轻松的完成了需求

场景三,你开发的Person 被广泛应用到别人的电脑

此时你老板认为世界上只有男和女,于是你的gender 采用了 boolean

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Person {

private String name;
private boolean gender;

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}
public boolean isMale() {
return gender;
}

public void setGender(boolean gender) {
this.gender = gender;
}
}

过了1年后,你老板发现还真有其他性别。你就不得不改变gender 为 String

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package hello.service;

import java.util.Objects;

public class Person {

private String name;
private String gender;

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}
public boolean isMale() {
// return gender == "M"; 可能会空指针
// return "M".equals(gender); 非空对象前置
return Objects.equals(gender,"M");
}
// 废弃的注解
@Deprecated
public void setGender(boolean gender) {
this.gender = gender ? "M" : "F" ;
}
}

此时使用者无需任何改变,依然能正常使用

封装的实现

包的功能就是提供访问控制,一种边界,封装的边界

  • public 任何人都能访问
  • protected 只有子类和同包的可以访问
  • package private(包级私有)包权限 同包可访问
  • private 只有自己能访问

包是没有嵌套包含关系的!!!跟文件夹父子目录不一样

JavaBean约定

  • getter
  • setter
1
2
3
4
public class Person {
private String name;
private boolean cute;
}

此时 Person 的 name / cute 无法被外界访问因为是 private

设置getter/setter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Person {

private String name;
private boolean cute;

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public boolean isCute() {
return cute;
}

public void setCute(boolean cute) {
this.cute = cute;
}
}

为什么 getter/setter长这个样子,不是无缘无故,而是JavaBean约定

  • 我们知道 java 代表咖啡,而程序员很浪漫
  • java中创建对象了叫什么呢? 对象?太土了,程序员的浪漫促使它起了名字叫做 “Bean” 就是”豆”
  • JavaBean 就是咖啡豆

对于一个 JavaBean 来说 加入他有一个 getX() 和 setX() 方法,我们就认为它有一个 x 属性

规则如下
1
2
3
4
5
6
7
// 非 boolean 值
String name;
setName/getName

// boolean 值
boolean gender
setGender/isGender
这些约定有什么用呢?
  • 最重要之一就是 JSON 对象和字符串相互转换
1
2
3
4
5
6
// 如 js
var obj = {"name":"张三","age":"李四"};
// 序列化
JSON.stringify(obj);
// 反序列化
var obj2 = JSON.parse(`{"name":"张三","age":"李四"}`);

java常见序列化库 fastjson / gson / jackson

java中使用序列化库

maven里引入依赖

1
2
3
4
5
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.58</version>
</dependency>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class Cat {

private String name;
private boolean cute;

public Cat(String name, boolean cute) {
this.name = name;
this.cute = cute;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public boolean isCute() {
return cute;
}

public void setCute(boolean cute) {
this.cute = cute;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.json;
import com.alibaba.fastjson.JSON;

public class Main {
public static void main(String[] args) {

Cat cat = new Cat("a",true);

System.out.printf(JSON.toJSONString(cat));

String s = "{\"cute\":true,\"name\":\"喵\"}";

cat = JSON.parseObject(s,Cat.class);
}
}

以上就是 JSON 的序列化和反序列化过程

JavaBean 约定

会使用你的 getter/setter 当作属性的名字,而不是你的成员private类型成员

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package com.json;

public class Cat {
private boolean cute;

public Cat(String name, boolean cute) {
this.cute = cute;
}

public String getName() {
return "123";
}

public void setName(String name) {
}

public boolean isCute() {
return cute;
}

public void setCute(boolean cute) {
this.cute = cute;
}
}

// name属性不存在了
// getter/setter还在

public class Main {
public static void main(String[] args) {

Cat cat = new Cat("a",true);
System.out.printf(JSON.toJSONString(cat));
//{"cute":true,"name":"123"}
}
}

JavaBean总结

在Java的世界中,对json进行读写的时候,我们只看JavaBean的 getter/setter方法,而不看是否具有xxx属性

这也进一步验证了我们想要达到封装的目的,封装应该尽可能的隐藏内部实现细节,而仅仅像外界暴露接口

暴露的接口 在JavaBean中就是 getter/setter 方法

虽然每次java里设置一堆getter/setter很繁琐啰嗦,好处就是为你提供了封装,

  • 封装是软件得以成功演进的保证

设计模式:抽象工厂方法

推荐一本书 effective java,无论处在java任何阶段都非常值得一读

  • 使用静态工厂方法代替构造器
  • 将构造器私有化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static Cat newCuteCat(String name){
return new Cat(name,true);
}

public static Cat newUnCuteCat(String name){
return new Cat(name,false);
}

public Cat(String name, boolean cute) {
this.name = name;
this.cute = cute;
}

// 静态工厂方法,可以有个名字,清楚无误的告诉你干什么
Cat.newCuteCat("xxx") // 萌的猫
Cat.newUnCuteCat("xxx") // 不萌的猫
new Cat("xxx",true) // 看不出来
优点
  1. 不像构造器,它是有名字的。可以描述在做什么
    • 疑问,我可以通过注释来告诉别人构造器做什么
    • 注释是不会被编译器处理的,因此它很有可能过时,过时的注释很可能会误导你,一个过时的注释比没有注释更糟糕
    • 尽可能不要写注释,如果你不能保证及时更新
  2. 静态方法不一定创建一个实例,你可以返回一个null也可以返回一个之前创建好的对象,但是构造器一定会创建一个实例
  3. 静态构造方法可以返回 该类的子类型,而构造器只能返回该类的实例
  4. 可以根据参数决定 要不要创建这个对象,我要创建什么对象,以及要不要把之前的对象缓存一下

    • 参考Boolean

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      public final class Boolean implements java.io.Serializable,
      Comparable<Boolean>
      {
      // 预先定义好的对象
      public static final Boolean TRUE = new Boolean(true);
      public static final Boolean FALSE = new Boolean(false);

      public static Boolean valueOf(boolean b) {
      return (b ? TRUE : FALSE);
      }
      ...
      }
    • 它返回了预先定义好的对象,不用每次都创建,省内存

  5. 静态工厂返回的这个对象,它可以不存在
    • 动态加载,灵活的体现
缺点
  1. 一个子类构造器会自动调用父类的构造器,构造对应的对象,但是静态方法不可以
  2. 很难让开发者找到,因为它的灵活性只能打开文档找到它 而不像这样new Cat()方便找到
静态方法的最佳实践
  • 将构造器变为私有,此时外界无法创建实例,只能通过暴露的工厂方法
  • 此时你内部如何修改构造器都随意了。外界只能操作暴露的工厂方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Cat {

private String name;
private boolean cute;

public static Cat newCuteCat(String name){
return new Cat(name,true);
}

public static Cat newUnCuteCat(String name){
return new Cat(name,false);
}

private Cat(String name, boolean cute) {
this.name = name;
this.cute = cute;
}
}

类的访问控制

这就是封装在类级别的表现

1
2
3
4
5
6
7
8
9
// 之前,不同包可以直接使用
public class Cat{

}

// 现在 包级私有 package private
class Cat{

}
  • 包级私有类 如ProcessEnvironment 在其他包外不能调用。

如何访问一个 包级私有的类呢?(不建议使用太hack)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 假设一个类,它是 包级私有 只能同包访问
package com.github.demo;
class A{}


// 在你的 maven项目里创建一个 桥接类 ,它的包路径和 上面的 一样
package com.github.demo;
class 桥接类{
public A newInstance(){
// 访问同一包中的私有类
return new A();
}
}

// 此时你去创建这个类
桥接类.newInstance();// 报错 因为java发现返回值 还是不能访问

// 怎么办呢,修改返回值类型为 Object
class 桥接类{
public Object newInstance(){
// 访问同一包中的私有类
return new A();
}
}

// 因为 任何对象都是 Object 的子类
// 此时你就可以
桥接类.newInstance();

既然这样那么我们是不是可以这样绕过限制创建 ProcessEnvironment 的实例呢?

1
2
3
4
5
6
// 你就创建这样的package
package java.lang;
public class MyClass{}
//此时报错了。 说这个类是被禁止的。

原因是 以 java开头的包都是 jvm的保留包,不允许你自定义一个java.lang包的,但是你可以通过别的包的访问限制

私有内部类

一个类有简单功能,你不想几个目录跳过来跳过去的使用

1
2
3
4
5
6
7
public class Home{

// 只能在同一个类中访问
private static class InnerClass{

}
}

Java模块系统简介

需求:你像把若干个包封装在一起,暴露接口出去

  • java8之前是不行的
  • java9引入了模块化系统,你可以把包封装成模块 module

java9的模块化系统好处是提供了更大范围的封装。但是它太新了。没有被业界所接受。

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
假设你是管理者,你手下有工人
package com.farm;
public class Manger{
private Worker worker;

public void manage(){
worker.work();
}
}

package com.farm;
public class Worker{
public void work(){
}
}

// 此时你的类发布了,此时你不小心暴露了 worker

另一个项目中,利用同包路径来访问你的worker

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.farm;
public class Boss{
Manager manager;

public void runCompany(){
manager.manage();
}

// Boss直接指挥工人,此时 manager就懵逼了,你怎么直接指挥工人了
public void directWorker(){
Worker w1 = new Worker();
w1.work();
}
}

为了不让 Boss 直接指挥 worker

1
2
3
4
// 你只能包级私有
class Worker{

}

但是有时候,出于其他原因你不得不把它自己包里面

  • 导致 manager 无法指挥 worker了
  • 只能 public了,一旦 public 你的老板又开始指挥工人了
  • 我们只能使用一些君子协定如 internal 包名 让人知道是内部的包。但是别人不君子咋办!!!
    1
    2
    3
    4
    package com.farm.worker.internal;
    public class Worker{
    ...
    }
1
2
3
4
package com.farm.worker;
class Worker{

}

java8之前无法做到,技术上做不到
java9的模块化系统可以解决,但是太新了,业界还没接受

builder模式

  • 简略版
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.json;

public class Person {
private String firstName;
private String lastName;
private String desc;
private String job;
private String phone;
private String address;

public Person(String firstName, String lastName, String desc, String job, String phone, String address) {
this.firstName = firstName;
this.lastName = lastName;
this.desc = desc;
this.job = job;
this.phone = phone;
this.address = address;
}
}
  • 一个 Person 有诸多属性你在创建的时候,顺序错了非常难发现,
  • 尤其是在 代码 review 的时候,不再 IDEA 里 ,没有参数提示。
  • 这个时候你可以使用 builder 安装 builder
  • 在IDEA里右键就可以创建 builder

此时可以这样

1
2
3
4
5
6
7
8
Person person1 = new Person("","","","","","")

// 更加直观,链式调用
Person person = PersonBuilder.aPerson()
.withFirstName("")
.withLastName("")
.withAddress("")
.build();

ZB-010-java对象系统

什么是对象

  • 对象就是数据和行为的集合
  • 一切能用 new 创建出来的都是对象
    1
    2
    3
    4
    new Object()

    特例 Integer a = 1; 实际是 Integer a = new Integer(1);
    特例 String a = ""; 实际是 String a = new String("");

对象的组成

  • 所有的对象都在堆上分配
  • 每个对象有自己的数据(成员变量)
    • 原生类型成员
    • 引用类型成员
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Home{
int age;
Cat cat;
Home(Cat cat){
this.cat = cat;
}
public static void main(String[] args){
new Home(new Cat(1,"阿三"));
}

public void miao(){
System.out.println("喵");
}
}
class Cat{
int age;
String name;
Cat(int age,String name){
this.age = age;
this.name = name;
}
}

对象的构造函数

  • 新建对象的唯一途径
    1. 在 堆 上分配空间
    2. 执行必要的初始化函数
    3. 执行构造函数
  • 没有构造器,则编译器偷偷生成一个
1
2
3
4
5
6
7
8
public class Cat{
int age; // 默认初始化为 0
byte b;// 0
short c;// 0
float d;// 0f
double e;// 0d
String f; // null 引用类型为 null
}

new Cat(1,"张三") 做了什么

1
2
3
4
5
6
7
8
class Cat{
int age;
String name;
Cat(int age,String name){
this.age = age;
this.name = name;
}
}
  1. 在堆上 开辟一个空间
  2. 对空间内的对象执行初始化语句,成员变量进行初始化 age = 0 ,name=null
  3. 执行类的构造器函数,对成员变量进行赋值 age = 1, name="张三"

对象的方法

  • 数据是“对象有什么”
  • 方法是“对象做什么”

方法的重载

  • 重载 overload
  • 重写/覆盖 override
  • 如何区分同名的不同重载方法?
    • 根据类型
    • 那隐式转换呢?
    • 类型最匹配优先(如果能匹配多个呢? null)
  • 能仅仅重载返回值吗?
  • 如何为方法提供默认值?
    • 没办法
    • 但是可以通过重载曲线救国

重载:根据类型区分

1
2
3
4
5
6
7
8
9
10
11
12
public class Cat{
Cat(){}

void f(){}
void f(String s){}

public static void main(String[] args){
Cat cat = new Cat();
cat.f();
cat.f("");
}
}

重载的坑

  • 装箱类型 int / Integer
  • Integer 是 Number的子类
  • Number 是 Object 的子类
1
2
3
4
5
6
7
8
9
10
11
12
13
public class Cat{

void f(int i){}
void f(Integer i){}
void f(Number i){}
void f(Object i){}

public static void main(String[] args){
Cat cat = new Cat();
// 此时匹配到哪一个呢?
cat.f(1);
}
}

如果一个方法调用可以匹配多个方法声明,我该调用谁?

  • 类型最匹配优先

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
     // int 此时会匹配 f(int i) 那个方法
    public class Cat{

    void f(int i){}
    void f(Integer i){}
    void f(Number i){}
    void f(Object i){}

    public static void main(String[] args){
    Cat cat = new Cat();
    // 此时匹配到哪一个呢?
    cat.f(1); // int
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // 变化了 注释掉这个 void f(int i){}
    public class Cat{

    void f(Integer i){}
    void f(Number i){}
    void f(Object i){}

    public static void main(String[] args){
    Cat cat = new Cat();
    // 此时匹配到哪一个呢?
    // Integer最匹配
    cat.f(1);
    }
    }
  • null值怎么办

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public class Cat{

    void f(Integer i){}
    void f(Object[] i){}

    public static void main(String[] args){
    Cat cat = new Cat();
    // 此时匹配到哪一个呢? 两个f都匹配
    // cat.f(null); //标红了
    cat.f((Integer)null); // 强制为 null 指定类型
    }
    }

    而我这样的时候就不报错了

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    public class Cat{

    void f(Integer i){}
    void f(Number i){}

    public static void main(String[] args){
    Cat cat = new Cat();
    // 此时匹配到哪一个呢?此时竟然不报错了
    cat.f(null); // Integer
    }
    }

    /*
    因为你要记住这句 匹配最接近的
    java的类型树

    假设我有两种类型 是父子关系。意味着匹配最接近的 就是 Integer
    -|Object
    --|Number
    ----|Integer


    假设是兄弟关系呢? 他们俩与 null的距离相同。所以歧义产生了 ,所以必须要 强制转换
    -|Object
    --|List
    ----|ArrayList

    -|Object
    --|Number
    ----|Integer
    */

能仅仅重载返回值吗?

1
2
3
4
5
6
7
8
9
10
void f(){}

int f(){
return 1;
}

// 标红
// 因为方法调用允许忽略返回值,
Cat c = new Cat();
c.f(); // 忽略返回值的时候,int f()方法会和 void f()产生冲突,编译器不知道要用哪个,因此不允许这样

高深的地方

  • 在源代码里是非法的,但在java的字节码里是允许存在的(JVM里允许)
  • 对于我们来说,编译器不让你做就别做!!!
1
2
3
void f(){}
int f(){return 1;}
// 在源代码里是非法的,但在java的字节码里是允许存在的。

如何为方法提供默认值

  • 答案是在java是不行的,只能这样
1
2
3
4
5
6
7
public void a(){
System.out.println("喵");
}

public void a(String a){
System.out.println(a);
}

而在其他语言,如js

1
2
3
function a(val = "miao"){
console.log(val);
}

构造器重载的例子

  • HashMap

对象的初始化过程

  • 必要的初始化工作
    1. 静态成员初始化
    2. 静态初始化块
    3. 成员初始化
    4. 初始化块
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class Cat{
static int a = 123;
static {
// 这里不能调用 成员方法 f()
System.out.println("000");
}
int age;
String name;
{
//
System.out.println("abc");
// 这里可以调用实例方法吗? 可以的
// 但是这是非常危险的,因为此阶段 还未调用 构造器 实例对象是残缺不全的
f();
}

void f(){}

Cat(int age,String name){
this.age = age;
this.name = name;
}

public static void main(String[] args){
new Cat(1,"aaa");
}
}

执行步骤

  1. 执行入口函数 main之前要先加载类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    第一步
    static int a = 123;
    第二步
    static {
    // 这里不能调用 成员方法 f()
    System.out.println("000");
    }
    第三步
    执行 main 函数 new Cat(1,"aaa"); 开辟内存空间
  2. 成员初始化

    1
    2
    3
    4
    5
    6
    7
    第四步
    int age = 0;
    String name = "";
    第五步
    执行成员方法块
    第六步
    执行构造器

对象的生命周期

  • 如果一直新建对象,内存会不会爆?

    • 可能会

      1
      2
      3
      4
      5
      6
      7
      Object[] arr = new Object[10000];
      for(int i=0;i<10000;i++){
      // 每次开辟1MB的空间
      arr[i] = new byte[1024*1024];
      }

      // 报错 OutOfMemoryError
    • 可能不会, jvm帮你回收 new出来无用的空间

  • 那对象的内存什么时候回收? 谁也不知道,JVM帮你做

  • 对象的内存如何被回收? 不用管,垃圾回收器帮你干
    • GC 垃圾回收器(幕后偷偷帮你做)
  • JVM怎么知道那个对象没有被用到?
    • 通过引用链 (GC Roots)
    • 沿着 GC Roots 可达的路径都是活对象,除此之外都是死对象

生活实例

1
2
3
4
5
6
7
8
9
10
11
内存 =  假设你房子的空间
GC Root =
房间里有一个 你
你.手 = 键盘
键盘.usb = 笔记本
笔记本.usb2 = 鼠标
笔记本.usb3 = 电源

此时预定义 GCRoot 根就是 “你”
沿着你能找到的所有对象都是好的
地上扔这个喝完的易拉罐,明显是垃圾 可以被干掉了

对象没有被引用,应该就被回收了吧?

1
2
3
4
5
这个东西什么时候回收,由不得你。
由JVM自己决定。 可能马上,也可能一直不回收

GC 也是需要耗费内存空间的。
假设你的空间非常小,GC就会帮你干掉

不可达的对象会被删除还是什么?

  • GC 算法(现在的jvm用的是 分代 回收算法)

简单理解就是,一块地上盖了楼,现在拆了盖新的楼

分代回收的大概过程

  • 不一定只发生删除
  • 还会发生压缩

在内存中除了 占地方 还有一个非常恐怖的事 就是碎片化

1
2
3
4
5
6
7
8
9
10
比如操场上开始广播体操。 每个人都拉开距离把操场占满了。 每个人占据的实际空间并不大。但是空间被割裂为很碎很碎

虽然有很大的空间。

但是此时 如果想在 操场中心建一个花园。 你就做不到了因为 人的分布已经把操场碎片化了。
以至于你不能找到一块 完整的连续的空间 做你想做的事情。
这时候也会抛出内存不足的Error

因此垃圾回收不仅仅是删除
还可能把这些细碎的内存 “归整” 来释放 完整的连续的空间供其他程序使用

ZB-009-java控制流

方法的控制流

  • 方法调用,就是一个方法栈每当开始一个方法调用的时候就在方法栈上面落上一个方法块(栈帧),方法调用结束的时候销毁栈帧,控制权交给上一个方法。然后循环往复

while和 do while

死循环

1
2
3
while(true){

}

do while

1
2
3
4
5
6
7
8
9
10
11
12
int i = 0;
while(i <100){
i+=1;
}



int j =0;
do{
System.out.println(i)
j++;
}while(j>0);

for 和 foreach循环

  • for循环
1
2
3
for(int i=0;i<100;i++){
System.out.println(i);
}
  • foreach循环(不是java独有的)
    1
    2
    3
    4
    // 冒号后面的对象必须实现了  Iterable 接口
    for(String e:Iterable<String>){
    System.out.println(e);
    }

foreach

1
2
3
4
List<String> list = Arrays.asList("1","2");
for(String e:list){
System.out.println(e);
}

break 和 continue

  • break 立刻结束包裹当前 break 的第一层循环
  • continue 跳过包裹当前 continue 的第一层循环中的语句,继续下一次循环
  • break label;

远古黑魔法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static void main(String[] args){
int i = 0;
// 这样写是声明了一个 label
http://google.com
i++; // label后面必须有一个语句 否则报错
}

~~~~~~~~~~~~~~~~~~~~~~~~~~
// 另一种用法
最外层循环:
for(int i=0;i<100;i++){
for(int j=0;j<100;j++){
break 最外层循环;
}
}

goto 上古时代控制程序流程的方式

  • java保留字,不能使用的关键字
1
2
3
4
5
6
// 其他语言 如 C
label:
if(i<100){
i++;
goto label;
}

switch

在java中只有三种东西可 switch

  • int/long/char/byte/short
  • enum
  • String(JDK7 2011年)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int i = new Random().nextInt(5);
switch(i){
case 0:
xxx;
break;
case 1:
xxx;
break;
case 2:
xxx;
break;
case 3:
xxx;
break;
case 4:
xxx;
break;
default:
xxx;
}

switch 可以贯穿

swich的作用域

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int i = new Random().nextInt(5);
switch(i){
case 0:
case 1:
case 2:{
String s = "A";
break;
}
case 3:{
String s = "A";
break;
}
default:
xxx;
}

ZB-008-java运算系统

基本运算符

1
2
3
4
5
6
7
8
9
10
11
+
-
*
/
% 取余 取余运算是带符号运算

+=
-=
*=
/=
%=
1
2
3
4
5
6
7
8
9
10
11
int a = 1 + 1; // int
int b = 5 / 2; // int

5.0/2; double


// 是否是奇数
public static boolean isOdd(int n){
reutrn n % 2 !=0;
// n % 2 == 1; 取余是带符号运算
}

自增自减

  • ++
  • --

i++++i 不一样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int a = 0;
int b = 0;

System.out.println(a++); // 0
System.out.println(++b); // 1


a++ 分两步
1. a = 0 作为表达式的值
2. a = a + 1

++b 分两步
1. a = a + 1 作为表达式的值
2. 把结果作为表达式的值

比较运算符

  • >
  • <
  • >=
  • <=
  • ==
  • !=

逻辑运算符

  • &&
  • ||
  • !
  • 短路特性
1
2
3
4
5
6
7
8
9
10
Boolean b = ?

// 1 true => ture
// 2 false => false
// 3 null => false

// 使用短路特性,如果不是 b = null的时候报错 空指针
if( b != null && b){

}

三元运算

  • ?:
1
2
int a = 3;
int res = a > 3 ? 1: -1;

位运算符

  • ~按位取反
  • & &= 按位与
  • | |= 按位或
  • ^ ^= 异或
  • << <<= 带符号左移
  • >> >>= 带符号右移
  • >>> >>>= 无符号右移(总是补0)

取反

计算机中 负数使用 补码表示的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int a = 3;
存在计算机里是 32位 4字节
00000000 00000000 00000000 00000011

~a 得到什么? -4 为啥

补码 ===> 按位取反
0000 0011 数字3
第一步 按位取反
1111 1100
第二步 补码 = 反码+1
1111 1101 得到数字-3

如果首位为“符号位” 那么
00000011 是数字 3
10000011 是数字 -3

如果是无符号就要用到补码了

烧脑的来了

+ - * / 是在电脑中的 CPU 负责运算的,是一种硬件电路

  • 对于基本的运算 + - 我们能不能统一为一种电路

    1
    就是一种 加法 的运算 既可以处理 "+" 也可以处理 “-”
  • 这种背景下

  • 补码出现了

例子 5 - 3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
5 - 3 等价于 5 + (-3)

这样就可以用 “加法” 来完成 5 - 3 的运算


5 的二进制表示
0000 0101

3 的二进制表示
0000 0011

-3 的二进制表示
0000 0011 数字 3 取反
1111 1100 反码 计算机里代表 -4
1111 1101 补码

5 + (-3)
0000 0101
1111 1101
-------------
0000 0010

& &= 按位与| |= 按位或

1
2
3
4
5
6
7
8
9
10
11
12
13
14
3 按位与 5

0000 0011 => 5
0000 0101 => 3
————————————
0000 0001 => 1


6 按位或 8

0000 0110 => 6
0000 1000 => 8
————————————
0000 1110 => E

异或

1
2
3
4
5
6
7
5 异或 7
0000 0101 => 5
0000 0111 => 7
_____________
0000 0010 => 2

不进位加法

NB的异或公式

  • 用处就是有些算法题可以用来加速计算
    1
    2
    在一个很长的数组里找一个数字 独立存在的数字 答案是 3
    [11,22,33,44,12,12,4,3,4] 把所有数字全部异或一次
1
2
3
x ^ y ^ y = x

x ^ x = 0

左移<<

  • 左移的好处就是比乘法操作快
1
2
3
4
5
6
7
8
9
数字 5
0000 0101
<<
0000 1010 得到 10

每左移一位 实际就是 *2

5左移1位 ==> 5*2 = 10
5左移2位 ==> 5*4 = 20

右移 >>

  • 带符号 首位补 原先的符号位
  • 不带符号 首位补0

位运算的场景

  • Modifier类

假设你有猫的 32个boolean值属性 萌不萌 胖不胖

  • 第一种方式 存放32个 boolean 成员属性,这样导致每个布尔值(1字节)占据 32字节
  • 另一种选择:一个int是 32位01组成,每位代表之前的boolean属性 这样 从32个字节 变成了 4字节
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class Cat{
public static final int 萌不萌 = 0x00000001;
public static final int 胖不胖 = 0x00000002;
public static final int 爱吃鱼 = 0x00000004;
public static final int 爱老鼠 = 0x00000008;
// 等等...

public static int final state =
Cat.萌不萌 | Cat.胖不胖 | Cat.爱吃鱼 | Cat.爱老鼠;
}

0x00000001 => 16进制最后一位换成4位2进制数 0001 萌不萌
0x00000002 => 16进制最后一位换成4位2进制数 0010 胖不胖
0x00000004 => 16进制最后一位换成4位2进制数 0100 爱吃鱼
0x00000008 => 16进制最后一位换成4位2进制数 1000 爱老鼠

正好 每个位置的 “ 1 ” 都是错开的

此时设置属性的时候只要 “或”
public static int final state =
Cat.萌不萌 | Cat.胖不胖 | Cat.爱吃鱼 | Cat.爱老鼠;
// 按位或之后得到 0000 1111

那么我们如何知道 Cat 萌不萌 呢?
只需要它让一个定义好的数字 & 运算就行了

// 萌不萌?
1111 // 全部的 state
0001 // 萌不萌的实际取值
// 按位与的结果为
res = 0001

结果只要比较 res 是不是 0 即可
  • 设置值时候使用 按位或 |
  • 取值值时候使用 按位与 &

参考练习题

字符串加法操作

1
2
3
4
5
int a = 1;
int b = 2;
System.out.println("a=" + a + ",b=" + b);

// 你可以给 StringBuilder 打断点,因为字符串是不可变字符序列,所以 频繁拼接很浪费空间,于是会使用 StringBuilder 对一块内存进行操作
  • 当你进行字符串加法的时候:它会调用这个对象的 toString, (a.toString(),b.toString())
  • 然后在你进行很长的字符串加法时候,JDK内部会自动的帮你转化成 StringBuilder 调用,减轻内存压力,提高性能。

判断 char 类型 合法字符问题

1
2
3
4
5
6
7
// 一个合法的十六进制的字符是:字符0-9,以及字符A/a/B/b/C/c/D/d/E/e/F/f (大小写都是合法的)
// 编写一个方法,给定一个字符,若是合法的十六进制字符,返回true,否则返回false
public static boolean isValidHexCharacter(char ch) {
return (ch >= '0' && ch <= '9')
|| (ch >= 'a' && ch <= 'f')
|| (ch >= 'A' && ch <= 'F');
}

ZB-007-java数据类型

数据类型

  • int 4字节 (4bytes 32bit)

    1
    2
    3
    int a = 10;
    int b = 011; //8进制
    int c = 0x11; // 16进制
  • char 2字节

  • short 2字节

原生数据类型和引用数据类型

原生 引用
int a= 1 String s = “a”
4字节存放 1这个值 假设 地址值:1234 存放 字符串”a”
  • 只要能找到对应的类,就是引用数据类型
  • 否则是原生数据类型

基本数据类型

  • byte 1字节
  • short 2字节
  • int 4字节(最大21亿)
    针对淘宝这种亿级用户最好不要用int使用 long
  • long 8字节
  • float 4字节
  • double 8字节
  • char 2字节
  • boolean true/false
  • void? 不返回任何类型
1
2
3
4
5
6
7
8
9
10
int a = 1;
byte b = (byte)128; // 会丢失精度
long c = 22_0000_0000L; // 不推荐用 "l" 与 “1” 容易歧义
float d = 0.1f;
double e = 2e-3; //科学计数法


//
int f = 0x123; // 十六进制
int g = 0b1010101; // 二进制, java7之后出现的

如果你想知道原生数据类型的最大最小值是什么?

  • 每一个原生数据类型对应一个装箱数据类型
  • 对应的装箱数据类型里有两个常量 MIN_VALUE/MAX_VALUE

问题来了?如果强行存储超过范围的值会怎么样?

  • 会溢出

浮点数是小数,在计算机中是近似表示

  • 浮点数只能比较大小,不要比较“相等
1
2
3
4
5
6
7
8
9
10
// 
float i = 0.1f;
if(i==0.1f){

}

double d = 0.2;
if(Math.abs(d - 0.2) < 0.000001){

}

类型转换

  1. 整数除法是地板除
  2. 将所有类型提升到最⾼精度进⾏计算

    1
    2
    3
    int i = 1 + 1; // ok
    int b = 1 + 1.0;// not ,因为 1.0是双精度所以 类型都被提升了, 所以 结果是 double 值 需要强制类型转化
    int c = (int)(1 + 1.0); // ok
  3. 丢失精度时需要进⾏强制转换

  4. char参与计算时使⽤ASCII码(Unicode码)
    1
    2
    3
    4
    5
    6
    7
    8
    char c1 = 'a';
    char c2 = 'A';
    char c3 = '你';

    char c4 = '1'; // 对应ASC码 为 49
    // c4 = c4 + 1; // 报错因为 + 1 是和 int值进行计
    算 必须进行强制转换
    c4 = (char)(c4 + 1); // c4的值是49 ,49+1 =50 ,50对应的字符是 '2'
  • 自动转换

    1
    2
    byte b = 100;
    int i = b; // 可以直接转换
  • 强制转换

    1
    2
    int i = 200;
    byte b = (byte) i; // 因为已经溢出了,所以要强制转换

整数除法计算是 地板除(向下取整)

1
2
3
4
5
6
7
8
9
10
11
12
13
int a = 3;
int b = 2;
int c = a / b; // 1


// 因为设置了返回值是 double 所以 result 会自动提升为 double
public static double divide(int a ,int b){
int result = a / b;
return result;
}


divide(3,2) // 1.0

精度问题,所有表达式中,参与计算的表达式都会提升到最高的精度进行计算,最后的结果也是最高精度

1
2
3
4
5
int i = 1 + 1; // 没问题

// int i2 = 1 + 1.0; // 有问题 与浮点数计算会变成double
// 只能这样 强制转换
int i2 = (int)(1 + 1.0);

解决整型地板除损失精度问题

1
2
3
4
5
6
7
8
9
10
11
public static double divide(int a ,int b){
double result = 1.0 * a / b;
return result;
}

// 或者

public static double divide(int a ,int b){
double result = (double)a / b;
return result;
}

void 类型

  • 所有的类型可以 类型.class 得到它的 class值
1
2
3
Class c = String.class;
Class intCLass = int.class;
Class voidCLass = void.class;

装箱类型(引用类型)

  • Byte(byte)
  • Short(short)
  • Integer(int)
  • Long(long)
  • Float(float)
  • Double(double)
  • Character(char)
  • Boolean(boolean)
  • Void?(void)

任何原生数据类型都有对应的装箱类型

自动装箱/自动拆箱

1
2
3
4
5
public static int add(int i){ return new Integer(i);}

int i = 100;
Integer integer = i; // 自动装箱
add(integer); // 自动拆箱

为什么要有引用类型

  • 容器类不接受原⽣数据类型
    1
    2
    3
    // List里只接受引用类型,不接受原生类型
    List<Integer> a = new ArrayList<>();
    a.add(1); // 丢进去的是 int 自动装箱 为 Integer
  • 可以赋值为null

    1
    2
    3
    4
    // boolean 只能有两种状态 true/false

    // 而装箱类型还有第三种状态 true/false/null
    Boolean state = null;
  • 提供额外的⽅法

    1
    2
    3
    4
    5
    String s = "123";
    // 内存中对应 是一个 char[] 数组

    // 而数字 内存中 4字节 01组成的序列
    int i = 123;
  • ⾃动装箱与拆箱

原⽣类型与引⽤类型带来的坑

  • 对null拆箱将引发空指针异常

    1
    2
    Integer a = null;
    int i = a;// 空指针异常
  • ==与equals约定

    1
    2
    3
    4
    5
    6
    7
    8
    9
    int a = 5;
    int b = 5;
    a == b // true

    Integer c = 1000; // 假设内存地址 512
    Integer d = 1000; // 假设内存地址 600
    c == d // false 因为 “==“ 比较的是值,而引用类型的值是 ”地址“ 512 != 600

    c.equals(d) // true

“==”

比较二者是否相同

“equals()”

比较二者是否是不是相等

  • 让你惊讶的地方!!!

  • 让你惊讶的地方!!!

  • 让你惊讶的地方!!!

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    Integer e = 1;
    Integer f = 1;

    e == f // true 竟然相等了?
    e.equals(f) // true


    int类型是占据 4字节的空间,
    而 Object / Integer 这类引用数据类型 远比int占据的空间要多

    如果你频繁
    Ingeter i = 1; // 占据空间远远大于4字节
    Ingeter b = 2; // 占据空间远远大于4字节

    所以java偷偷做了一件事
    对于非常小的数字则认为是常用的数字。
    则对其进行缓存 具体参考 IntegerCache的文档
    会对 -128~+127的 数字进行缓存,而不是重新创建对象

顺便提一句:Integer对象是不可变的

1
2
3
Integer c = 1; // 内存地址 509
c = c + 1; // +1 操作 此时 c的地址变成了 512
// Integer 本身是不可变的

数组类型

面试题:请你给我解释一下数组是什么,为啥我没看见过数组类型的声明,却可以直接用呢?

  • 数组类型是一种特殊的类型,JDK负责创建它

由JDK负责创建的特殊类型

  • X[] x = new X[10] 长度为10的数组
  • X[] x = new X[]{5,4,3,2,1} 长度为5的数组
  • X[] x = {5,4,3,2,1}
  • 其中 X 也可以是数组类型
    1
    int[][] c = new int[][]{{1,2,3},{4,5,6}}

数组的主要特性

  • 长度不可变
  • 类型安全 String[] c = {"a","b"}; // 声明了是 String 就不能丢其他类型的东西
  • 只有一个 length 属性
  • 可以用下标去取对应元素 a[1]
  • 可以使用 for 循环

ZB-006-工具使用

maven

  • 提供一个生命周期流程
  • 每个插件可以把一小部分工作绑定到生命周期上
  • maven本身对这些事不做任何干涉,只定义生命周期
  • maven 每个阶段执行时都会把前面的每个阶段执行一遍
  • maven default lifecycle

参考资料

maven依赖的问题——传递性依赖

  • 你的项目依赖多个库 A、B、C
  • 而这个库A又依赖别的库 如C
  • 此时 多个地方依赖 C C的版本如果不一致就会有问题

maven显示依赖树

1
mvn dependency:tree

IDEA生产力翻倍的快捷键

  • 万能键 ALT + ENTER 任何情况都可以使用
  • 左侧项目结构面板右上角有个按钮 Scroll from Source 从源代码跳转
  • Search everywhere: 双击 shift
  • 查看定义:Declaration (command + B) (等价于按住command + 鼠标右键)
  • 查看⽂件:Navigate - File
  • ⾼级查找:Find in Path (顶栏Edit-Find-Find in Path) 几乎满足你查找的所有需求(command + shift + f)
  • 快速⽣成:Generate(ctrl + N)
  • 格式化:Reformat Code(alt + command + L)
  • 优化导⼊语句:Optimize imports(alt + command + O)
    • 比如你代码里开始用了 ArrayList,后来不用了,但是会被导入,这个时候用这个
  • 导航:Navigate - Back/Forward(alt + command + ⬅️ ➡️)
  • 所有的实现类:Implementation(command + 1)
  • 谁调⽤了这个⽅法:Call Hierarchy(command + 2)
  • ⽂件⼤纲:File Structure (command + 3)
    • 列出这个类的所有方法,成员
  • 下⼀处错误:Next Highlighted Error (command + 4)

  • ⾼级重命名:Rename (command + 5)

  • 调试器快捷键:F5/F6/F7/F8

推荐一个idea插件

  • Key Promoter X ,你点击ide的按钮的时候,能提示你快捷键

Git相关操作

  • 查找背锅侠:Annotate/Blame
  • 查看当前⽂件的历史版本
  • ⾼级筛选⽅式查找commit记录
  • 显示差异:show diff
  • Open in GitHub (选中你的代码 右键列表里)

底部工具栏 Version Control

  • Local Changes 等同于 git status 本地的变更还没有提交
  • Log 这个仓库里所有的日志

编程快捷键

1
2
3
4
5
6
7
8
9
psvm ==> public static void main(){}

sout ==> System.out.println()

soutv ==> System.out.println("xxx=" + xxx)

fori ==> for(int i = 0 ; i < ;i++)

fore ==> ...

寻找配对括号的小插件

  • Rainbow Brackets 彩虹括号
  • bytecode viewer 字节码查看器
  • Key Promoter X 快捷键提示插件
  • Stream java8流调试器,debug 的bar里最后一个按钮

调试器:没有解决不了的问题

为什么需要调试器

  • 理解程序执⾏的过程
  • 理解JVM的内部构造
  • ⾮常⽅便的检查在任意时间点JVM的内部状态

调试器

  • 单步跳过 ,Step over 执行一行,无论当前行的语句有多么的复杂。
  • 单步进⼊ , Step into 执行一个语句。进入这个方法的执行(最详细的方法执行)。
  • 单步跳出
  • 全速运⾏ Resume Program (F8)恢复程序,当前的程序全速跑到下一个断点
  • 断点(⾯板)
  • 条件断点 (所在行设置断点,点击右键弹出一个框 Condition 输入 i==5000 当i=5000的时候)
  • 跳转到源代码
  • Watch

JVM

  • 堆 所有对象都在堆上分配
  • 栈 (假设现在只有一个线程。每个线程有一个方法栈。方法的入口就是main方法。每次调用一个方法就会压栈,方法结束就会出栈)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class A {
public static void main(String[] args) {
a();
}

public static void a() {
System.out.println("a");
b();
}
public static void b() {
System.out.println("b");
c();
}
public static void c() {
int x = 1;
System.out.println("c");
}
}

函数一次一次调用形成的方法块叫什么

  • 栈帧
1
c执行结束 它里面的局部变量x会被销毁

执行表达式

1
2
3
打断点后,选中所在行右键选择Evaluate Expression

输入框里可写任意代码

鬼畜的用法,打破流程,不推荐用

1
return result; // 右键, View/Edit Text 修改为你要返回的内容

调试器的原理

  • 调试⾮项⽬源代码
  • 调试命令⾏程序

调试 mvn compile 过程的代码

  • 你想调试什么东西,首先要拿到这个东西的源代码
  • 比如调试 mvn compile,本质也是一个JVM
  • google 搜索maven compiler plugin github 把代码clone到本地
  • mvn 除了这个命令,还有一个调试版的命令 mvnDebug
  • 你可以这样运行 /Users/hjx/maven/apache-maven-3.6.3/bin/mvnDebug compile 这就是 debug模式运行 mvn
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 1。 找到你本地 mvn的 debug模式命令
> /Users/hjx/maven/apache-maven-3.6.2/bin/
mvnDebug compile

# 2。 debug 模式运行 mvn,监听在 8000端口
Preparing to execute Maven in debug mode
Listening for transport dt_socket at address: 8000

# 3。 此时在 IDEA里 编辑器右边顶部 的 add Configuration 里 “加号 + ”选择 Remote ,Port填入 8000,然后 ok
# 为什么是 Remote 这个过程不仅可以发生在你本地,还可以发生在你的生产服务器
# 然后在 mvn compile 源代码的 CompilerMojo 里 skipMain 这行 176

# 4。 然后 debug模式运行,你发现代码没有停在这里,不要开始怀疑人生, 要看你本地的 mvn版本 和代码的版本是否是一个版本

# 5。 源代码终端输入 git tag 找到跟你本地一致的版本号 如 3.6.2

# 6。 切换本地代码版本
git checkout maven-compiler-plugin-3.6.2

# 7。 在此 debug运行代码

此时你就可以调试 mvn compile 了

ZB-005-JAVA基本结构

JAVA介绍

  1. 强类型
  2. 静态编译
  3. 跨平台

代码比较死板,但是非常适合团队协作项目。

什么是操作系统

比如现在有 windows / mac / linux 三个系统,你要让他们做什么比如 打开文件,发请求就要使用操作系统的语言(API)和操作系统对话

这样就给软件开发者开了一个很大的难题。你开发了一个软件跑在三个系统上。就要用三个系统的API去分别编写。此时工作量变成了三倍!

举个例子,比如你要写一本书,三个系统好比三个国家。

  • 你使用了某个语言如 英语 English
  • 此时需要去三个国家找到对应懂英语的人。
  • 这样你只要在每个国家找到懂英语的人。你本来是要为每个国家开发一本书。现在你只需要开发一本书,然后让不同平台懂英语的人翻译成对应国家的语言。

这个会翻译的人 在JAVA中就叫 JVM 虚拟机

JVM负责与操作系统的接口打交道,此时你只要写一种语言如java,剩下的交给JVM将 java翻译成中间层的语言 byte code(字节码)

字节码

二进制,在计算机里只有0和1

java广告词

java语言的基本单元 类 和 包

class

  • java最小的结构是 类
  • 类必须放在与它同名的 .java文件中
1
2
3
4
5
// 程序最小的结构是 class
// 直接放在 src/main/java下的包叫做 “默认包”
public class Cat{

}

package

  • 每个类都处在一个包中
  • 包的名字由目录结构所确定
1
2
3
4
5
// 目录结构为: src/main/java/my/cute
package my.cute;
public class Cat{

}
推荐包名规则
  • java程序约定(我不强制你这么做,但我约定你这么做)
1
2
3
4
5
6
# 不建议这样
package my.cute;

# java推荐约定 包名以你互联网公司域名的反序来命名,这样可以避免命名冲突
package com.alibaba
package com.google

maven的项目约定

1
2
3
4
5
// 生产代码
src/main/java/my/cute/Cat.java

// 测试代码
src/test/java/my/cute/CatTest.java

包的意义

如果存在如下两个同名的类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
src/main/java/my/pet/Cat.java
src/main/java/my/pet2/Cat.java


// 如何在 src/main/java/my/Home.java里引用者两个 Cat呢?

// Home.java
package my;

import my.pet.Cat;
// import my.pet2.Cat;

public class Home{
Cat cat;
//全限定类名
my.pet2.Cat cat2;
}
  • 在JVM中所有的类 都会变成“全限定类名” Full Qualified Name (FQCN)
    1
    2
    3
    4
    5
    public class Home{
    // 最终都会变成
    my.pet.Cat cat;
    my.pet2.Cat cat2;
    }

这就是为什么我们需要“包” 最大的作用就是避免命名冲突,区分同名但不同的类

main函数

1
2
3
4
5
6
public class Cat{
// 程序的入口函数
public static void main(String[] args){

}
}
  • 疑问为什么String[] args 里的 String不用写包名 ,如果你用的是 IDEA按住 alt点击进去,发现 String的包名是 package java.lang
  • 如果你的类放在 java.lang 下 可以不经引入直接使用

方法调用

  • 在每个函数调用的时候,它会创建一个全新的函数的执行环境
  • 函数多次调用的过程之间,它们是相互独立的。
  • 函数可以被一次声明,然后多次调用,每次调用的执行环境都是独立的。
1
2
3
4
5
6
7
8
9
10
public class Cat{
public static void main(String[] args){
f(1);
f(2);
}

public static void f(int i){
return i + 1;
}
}

局部变量

  • 包围它的第一对花括号{}

静态成员变量

存在遥远的jvm的一个地方,独立于任何的对象。和任何对象都没关系

1
2
3
4
5
6
7
8
9
10
11
12
public class Cat{
public static int i; // 默认会初始化为0

public static void main(String[] args){
add();
add();
}

public static void add(){
i = i + 1;
}
}

对象/构造器和成员变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class Home{
public static void main(String[] args){
Cat cat = new Cat();
cat.name = "阿三";
cat.miao();

Cat cat2 = new Cat("阿大");
cat2.miao();

Cat cat3 = new Cat();
cat3.miao(); // 空指针 因为name是null
}
}

class Cat{
// 成员变量
private String name;
// 假如你没声明任何的构造器 ,编译器会帮你偷偷生产一个构造器
// Cat(){} // 你不写就自带一个

public Cat(){}
// 如果你写了其他构造器,将不再提供默认的无参构造器,而你想调用无参的构造器则要自己写。
public Cat(String name){
// this = 刚刚创建的Cat类的实例
this.name = name;
}
/*
public static void miao(){
// 报错,静态方法不能调用非静态成员
System.out.println("喵,我是"+ name)
}
*/

public void miao(){
System.out.println("喵,我是"+ name)
// 在不引起歧义的情况下,你可以 省略 this
System.out.println("喵,我是"+ this.name + "我的名字长度是" + name.length());
}
}

实例方法和空指针异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class Home{
public static void main(String[] args){
Cat cat = new Cat();
cat.name = "阿三";
cat.miao();

Cat cat2 = new Cat("阿大");
cat2.miao();

Cat cat3 = new Cat();
cat3.miao(); // 空指针 因为name是null
}
}

class Cat{
String name;
public Cat(){}
public Cat(String name){
this.name = name;
}

public void miao(){
// 非空判断 ,规避空指针
if(name == null){
System.out.println("我还没名字");
}else{
System.out.println("喵,我是"+ this.name + "我的名字长度是" + name.length());
}
}
}

对象和引用、值传递

ZB-004-FQ

如何FQ

  • 搜索 s*h*a*d*o*w*s*o*c*k*s,眼里有 “星星”,自行去“星星”
  • 下载客户端,大概 100每年

chrome自动识别是否FQ

  • google 商店搜索 SwitchyOmega
1
2
3
4
5
导入配置之后记得,选择自动切换。如果FQ发现失败。

代理的默认端口是1080,所以理论上不用改。
但是针对你失败有可能是 你小飞机的 socks5端口跟 chrome插件的不一样
就把agent那个里面的端口改成你小飞机里 socks5端口

命令行FQ

mac

1
2
3
4
5
6
7
# 1 安装 homebrew
/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

# 2 安装 proxychains-ng
brew install proxychains-ng

# 3 配置

配置

1 新建 ~/.proxychains.conf 文件

内容如下

1
2
3
4
5
6
7
8
9
10
strict_chain
quiet_mode
proxy_dns

remote_dns_subnet 224
tcp_read_time_out 15000
tcp_connect_time_out 8000
[ProxyList]
# 你的代理 如 ss 我的端口是 1086
socks5 127.0.0.1 1080

2 第二步 touch ~/.bashrc

1
echo 'alias pc="proxychains4 -f ~/.proxychains.conf"' >> ~/.bashrc source ~/.bashrc'

3 第三步 source ~/.bashrc

1
2
3
4
5
6
7
# 测试
pc curl -L https://twitter.com/

# 如果失败 看看你的 mac版本是否是 10.12之后
# 如果是
# 关闭SIP
https://www.jianshu.com/p/fe78d2036192