ReactWheels07-03Hooks全解2

代码仓库

用useReducer代替redux

一共七步

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
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
import React, { useContext, useReducer, useEffect } from "react";
// step01 将数据集中在store里
const store = {
user: null,
books: null,
movies: null
}
// step02 将所有操作集中在 reducer里
const reducer = (state, action) => {
switch (action.type) {
case 'setUser':
return { ...state, user: action.user }
case 'setBooks':
return { ...state, books: action.books }
case 'setMovies':
return { ...state, movies: action.movies }
default:
throw new Error('no match action')
}
};

// step03 创建 Context
const Context = React.createContext(null)

function ReducerDemo1() {
// step04 创建对数据的 读/写 API
const [state, dispatch] = useReducer(reducer, store)
return (
// step05 将 step04的 读写api放到 Context提供的组件上
// step06 Context.Provider 将 Context 提供给所有组件
<Context.Provider value={{ state, dispatch }}>
<User />
<hr />
<Books />
<Movies />
</Context.Provider>
);
}

// step07 各个组件用 useContext 获取读写API
function User() {
const { state, dispatch } = useContext(Context);
useEffect(() => {
ajax("/user").then(user => {
dispatch({ type: "setUser", user: user });
});
}, []);
return (
<div>
<h1>个人信息</h1>
<div>name: {state.user ? state.user.name : ""}</div>
</div>
);
}

function Books() {
const { state, dispatch } = useContext(Context);
useEffect(() => {
ajax("/books").then(books => {
dispatch({ type: "setBooks", books: books });
});
}, []);
return (
<div>
<h1>我的书籍</h1>
<ol>
{state.books ? state.books.map(book => <li key={book.id}>{book.name}</li>) : "加载中"}
</ol>
</div>
);
}

function Movies() {
const { state, dispatch } = useContext(Context);
useEffect(() => {
ajax("/movies").then(movies => {
dispatch({ type: "setMovies", movies: movies });
});
}, []);
return (
<div>
<h1>我的电影</h1>
<ol>
{state.movies
? state.movies.map(movie => <li key={movie.id}>{movie.name}</li>)
: "加载中"}
</ol>
</div>
);
}

export default ReducerDemo1;

// 帮助函数

// 假 ajax
// 两秒钟后,根据 path 返回一个对象,必定成功不会失败
function ajax(path) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (path === "/user") {
resolve({
id: 1,
name: "hjx"
});
} else if (path === "/books") {
resolve([
{
id: 1,
name: "JavaScript 高级程序设计"
},
{
id: 2,
name: "JavaScript 精粹"
}
]);
} else if (path === "/movies") {
resolve([
{
id: 1,
name: "爱在黎明破晓前"
},
{
id: 2,
name: "恋恋笔记本"
}
]);
}
}, 2000);
});
}

useContext

  • 上下文
    • 全局变量是全局的上下文
    • 上下文是局部的全局变量
  • 使用方法
    1
    2
    3
    step1 const C = createContext(null);
    step2 <C.Provider value={{ n, setN }}> 圈定作用域
    step3 在作用域内(任意级子组件内)使用 useContext(c) 来使用上下文
  • 不是响应式的
    • 你在一个组件内将 C变化 setN
    • 另一个模块不会感知这个变化
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
import React, { createContext, useState, useContext } from 'react';

const C = createContext(null);

function UseContextDemo1() {
const [n, setN] = useState(0)
return (
<C.Provider value={{ n, setN }}>
<div className="UseContextDemo1">
<Baba />
</div>
</C.Provider>
)
}

function Baba() {
return (
<div>
我是爸爸
<Child />
</div>
)
}

function Child() {
const { n, setN } = useContext(C);
const onClick = () => {
setN(i => i + 1)
}
return (
<div>
我是儿子,我得到n:{n}
<button onClick={onClick}>+1</button>
</div>
)
}
export default UseContextDemo1

useEffect

  • 副作用

    • 对环境的改变即为副作用,如 document.title = 'xxx'
    • 不一定把 这个副作用 放在 useEffect里
    • 每次 render后执行
  • 用途

    • 作为 componentDidMount 使用 [] 作为第二个参数
    • 作为 componentDidUpdate 使用 [n] 数组内可以指定依赖,n变了才会触发
    • 作为 componentWillUnmount 使用 ,通过return 代表销毁的时候
  • 特点

    • 存在多个 useEffect,会按照出现次序执行

Demo

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
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
import React, { useEffect, useState } from 'react';

function UseEffectDemo1() {
return (
<div className="demo1">
<Demo1 />
<Demo2 />
<Demo3 />
<Demo4 />
<Demo5 />
<Demo6 />
<Demo7 />
</div>
)
}


function Demo1() {
const [n, setN] = useState(0)

useEffect(() => {
// 任何一个变了变量变了都执行
// 第一次 和 第二三四 。。。 次
console.log('demo1:第一次 和 第二三四 。。。 次,任何一个变量变了都执行')
})

const onClick = () => {
setN(i => i + 1)
}
return (
<div className="demo1">
<h1>useEffect不传递第二个参数, 第一二三。。。次执行,任何一个变量变了都执行</h1>
n:{n}
<br />
<button onClick={onClick}>+1</button>
</div>
)
}

function Demo2() {
const [n, setN] = useState(0)
useEffect(() => {
// n更新之后
console.log('demo2:n更新之后')
}, [n])

const onClick = () => {
setN(i => i + 1)
}
return (
<div className="demo2">
<h1>useEffect传递第二个参数[n], 第一次执行,以及n变了执行</h1>
n:{n}
<br />
<button onClick={onClick}>+1</button>
</div>
)
}


function Demo3() {
const [x, setX] = useState(0)
const [y, setY] = useState(0)
useEffect(() => {
// x更新之后,
console.log('demo3:第一次执行,以及x更新之后才变,y改变不会触发effect')
}, [x])

const onClickX = () => {
setX(i => i + 1)
}
const onClickY = () => {
setY(i => i + 1)
}
return (
<div className="demo2">
<h1>useEffect传递第二个参数[x],第一次执行,以及x更新之后才变,y改变不会触发effect</h1>
x:{x}
<br />
y:{y}
<br />
<button onClick={onClickX}>x+1</button>
<button onClick={onClickY}>y+1</button>
</div>
)
}

function Demo4() {
const initValue = 0;
const [z, setZ] = useState(initValue)
useEffect(() => {
// 跳过第一次
if (z !== initValue) {
// 跳过第一次
console.log('demo4:通过if(z!== initValue)判断,跳过第一次,z修改后执行')
}
}, [z])

const onClick = () => {
setZ(i => i + 1)
}
return (
<div className="demo2">
z:{z}
<br />
<button onClick={onClick}>z+1</button>
</div>
)
}

function Demo5() {
const [z, setZ] = useState(0)
useEffect(() => {
console.log('demo5:useEffect 第二个参数为空数组[],只执行一次')
}, []);

const onClick = () => {
setZ(i => i + 1)
}
return (
<div className="demo5">
<h1>demo5:useEffect 第二个参数为空数组[],只执行一次</h1>
z:{z}
<br />
<button onClick={onClick}>z+1</button>
</div>
)
}

function Demo6() {
useEffect(() => {
const id = setInterval(() => {
console.log('demo6:useEffect 移除时执行 return里写一个函数')
}, 3000)
return () => {
window.clearInterval(id);
console.log('demo6:组件销毁的时候执行')
}
}, []);

return (
<div className="demo6">
<h1>demo6:useEffect 移除时执行操作 return一个函数进行操作</h1>
</div>
)
}


function Demo7() {
useEffect(() => {
console.log('demo7:1')
}, []);
useEffect(() => {
console.log('demo7:2')
}, []);

return (
<div className="demo7">
<h1>demo7:多个useEffect 按顺序执行</h1>
<br />
</div>
)
}

export default UseEffectDemo1

useLayoutEffext

Demo1

执行流程

  • App执行
  • vDom
  • 生成真实Dom
  • render完毕,改变外观 <div>0</div>
  • effect 执行=》div.innerText = 1000
1
2
3
4
5
6
7
8
9
10
11
function Demo1() {
const [n, setN] = useState(0)
useEffect(() => {
document.querySelector('#x').innerText = `n:1000`
}, [n])
return (
<div className="demo1">
<h1>useEffect</h1>
<div id="x">n:{n}</div>
</div>
)

Demo2

执行流程

  • App执行
  • vDom
  • 生成真实Dom
    • <div>0</div>
    • useLayoutEffect 执行=》div.innerText = 1000
    • 作为浏览器不会看见 n = 0 的过程
  • render完毕,改变外观 <div>1000</div>
1
2
3
4
5
6
7
8
9
10
11
12
function Demo2() {
const [n, setN] = useState(0)
useLayoutEffect(() => {
document.querySelector('#x2').innerText = `n:1000`
}, [n])
return (
<div className="demo2">
<h1>useEffect</h1>
<div id="x2">n:{n}</div>
</div>
)
}

useLayoutEffect 和 useEffect 区别

  • useLayoutEffect 是 render之前(看不到闪烁)
    • 是不是用这个更好呢?
      • 不是,因为大部分时间,我们不回去改变 dom
      • useEffect 满足不了需求的时候用 useLayoutEffect
      • 优先级更高
  • useEffect 是 render之后触发(能看见闪烁)

useLayoutEffect 和 useEffect 性能测试

  • useLayoutEffect 耗时更短
  • useEffect 耗时更长
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
function Demo3() {
const [x, setX] = useState(0)
const time = useRef(null)
const onClickX = () => {
setX(i => i + 1)
time.current = performance.now()
}

useEffect(() => {
if (time.current) {
console.log('demo3:' + (performance.now() - time.current))
}
}, [x])

return (
<div className="demo3">
<h1>useEffect的更新UI时间更长</h1>
x:{x}
<br />
<button onClick={onClickX}>x+1</button>
</div>
)
}

function Demo4() {
const [x, setX] = useState(0)
const time = useRef(null)

const onClickX = () => {
setX(i => i + 1)
time.current = performance.now()
}

useLayoutEffect(() => {
if (time.current) {
console.log('demo4:' + (performance.now() - time.current))
}
}, [x])

return (
<div className="demo4">
<h1>useLayoutEffect的更新UI时间更短</h1>
x:{x}
<br />
<button onClick={onClickX}>x+1</button>
</div>
)
}

useEffect注意事项

  • 如果 useLayoutEffect 和 useEffect 同时存在 useLayoutEffect先执行
  • 如果 用 useLayoutEffect ,你最好改变Layout
  • 为了用户体验,优先使用 useEffect(优先渲染)

ReactWheels07-03Hooks全解1

代码仓库

React Hooks

  • 状态:useState
  • 副作用: useEffect
    • useLayoutEffect
  • 上下文: useContext
  • Redux : useReducer
  • 记忆: useMemo
    • useCallback
  • 引用: useRef
    • useImperativeHandle
  • 自定义Hook
    • useDebugValue

useState

1
2
3
// 使用
const [n, setN] = useState(0);
const [user, setUser] = useState({name:'f'});

注意事项:不能局部更新

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const [user, setUser] = useState({name:'f',age:19});

onClick(){
setUser({
age:20
})
}

// 这样操作之后, name属性就没了

你只能这样

onClick(){
setUser({
...user,
age:20
})
}

注意事项2:不能直接改地址

1
2
3
4
5
6
7
const [user, setUser] = useState({name:'f',age:19});

onClick(){
user.name = 'xxx' // 千万不要改之前的对象
setUser(user)
// 这样也不会触发更新
}

useState补充

  • useState接受函数
1
2
3
4
5
6
7
8
9
10
// 这样每次 js解析 都会 算一下 18+18
const [user,setUser] = useState({name:'aaa',age:18+18})

对比这样的好处是 函数只会执行一次,减少多余的计算过程
const [user,setUser] = useState(()=>{
return ({name:'aaa',age:18+18})
})


// 基本用不到
  • setState 可以用函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const [n, setN] = useState(0);

onClick(){
setN(n+1) // n不会变
setN(n+2) // 只有最后一次会生效
// n = 2
}


// 多次操作n的正确姿势
onClick(){
setN(x => x+1)
setN(y => y+1)
// n = 2
}

useReducer

  • 用来践行 Flue/Redux思想 一共四步
    • 创建 initState
    • 创建所有 reducer(state,action)
    • 传给 useReducer ,得到 读/写 api
    • 调用写 {type:'操作类型'}
  • useReducer 就是 useState的复杂版

  • 代码

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
import React, { useState, useReducer } from "react";
import ReactDOM from "react-dom";

// 1创建初始值
const initial = {
n: 0
};

// 2创建所有 reducer
const reducer = (state, action) => {
if (action.type === "add") {
return { n: state.n + action.number };
} else if (action.type === "multi") {
return { n: state.n * 2 };
} else {
throw new Error("unknown type");
}
};

function App() {
const [state, dispatch] = useReducer(reducer, initial);
const { n } = state;
const onClick = () => {
dispatch({ type: "add", number: 1 });
};
const onClick2 = () => {
dispatch({ type: "add", number: 2 });
};
return (
<div className="App">
<h1>n: {n}</h1>
<button onClick={onClick}>+1</button>
<button onClick={onClick2}>+2</button>
</div>
);
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
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
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
import React, { useReducer } from "react";
import ReactDOM from "react-dom";

const initFormData = {
name: "",
age: 18,
nationality: "汉族"
};

function reducer(state, action) {
switch (action.type) {
case "patch":
return { ...state, ...action.formData };
case "reset":
return initFormData;
default:
throw new Error();
}
}

function App() {
const [formData, dispatch] = useReducer(reducer, initFormData);
// const patch = (key, value)=>{
// dispatch({ type: "patch", formData: { [key]: value } })
// }
const onSubmit = () => {};
const onReset = () => {
dispatch({ type: "reset" });
};
return (
<form onSubmit={onSubmit} onReset={onReset}>
<div>
<label>
姓名
<input
value={formData.name}
onChange={(e) =>
dispatch({ type: "patch", formData: { name: e.target.value } })
}
/>
</label>
</div>
<div>
<label>
年龄
<input
value={formData.age}
onChange={(e) =>
dispatch({ type: "patch", formData: { age: e.target.value } })
}
/>
</label>
</div>
<div>
<label>
民族
<input
value={formData.nationality}
onChange={(e) =>
dispatch({
type: "patch",
formData: { nationality: e.target.value }
})
}
/>
</label>
</div>
<div>
<button type="submit">提交</button>
<button type="reset">重置</button>
</div>
<hr />
{JSON.stringify(formData)}
</form>
);
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

ReactWheels07-02useState原理

useState原理

useState用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import React, { useState } from "react";
import ReactDOM from "react-dom";

function App() {
console.log("app 运行了");
const [n, setN] = useState(0);
console.log("n:" + n);
return (
<div className="App">
<h1>{n}</h1>
<button onClick={() => setN(n + 1)}>onAdd</button>
</div>
);
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

脑补过程

  • 首次渲染 render(<App/>)
  • 调用 App 函数 => 得到 虚拟div,然后创建真实的div
  • 用户点击 button,调用 setN(n+1)
  • 再次 render(<App/>)
    • 调用 App函数 ,
    • 得到 虚拟div
    • DOM Diff 更新真实 div
  • 每次都调用 App(),都会运行 useState(0)
    • 第一次的时候 n = 0
    • 之后每次 n = n+1

分析

setN

  • setN 一定会修改数据 x,将 n+1 存入 x
  • setN 一定会触发 <App/> 重新渲染 re-render

useState

  • useState 肯定会从 x 读取 n的最新值

x

  • 每个组件有自己的数据x,我们将它命名为 state

自己实现一个 useState

最简单的 useState 实现

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
import React from "react";
import ReactDOM from "react-dom";

let _state;
const myUseState = (initValue) => {
console.log("myUseState run");
_state = _state === undefined ? initValue : _state;
const setState = (newValue) => {
_state = newValue;
render();
};
return [_state, setState];
};
function render() {
ReactDOM.render(<App />, document.getElementById("root"));
}

function App() {
console.log("app 运行了");
const [n, setN] = myUseState(0);
console.log("n:" + n);
return (
<div className="App">
<h1>{n}</h1>
<button onClick={() => setN(n + 1)}>onAddN</button>
</div>
);
}
render();
  • 存在问题 ,App内 有两个变量的时候咋办 m,n 那样就会使用两次 useState

改进思路

把 _state 做成一个对象

  • _state = {n:0,m:0}
  • 不行,因为 useState(0) 的时候 不知道变量 叫做 n 还是 m

把 _state 做成数组

  • 比如 _state = [0,0]
  • 貌似可以

解决多个 值 setState问题

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
import React from "react";
import ReactDOM from "react-dom";

let _state = [];
let index = 0;
const myUseState = (initValue) => {
const currentIndex = index;
_state[currentIndex] =
_state[currentIndex] === undefined ? initValue : _state[currentIndex];
const setState = (newValue) => {
_state[currentIndex] = newValue;
render();
};
index += 1;
return [_state[currentIndex], setState];
};
function render() {
// 每次重新渲染的时候 重置 index
index = 0;
ReactDOM.render(<App />, document.getElementById("root"));
}

function App() {
console.log("app 运行了");
const [n, setN] = myUseState(0);
const [m, setM] = myUseState(0);
console.log("n:" + n);
return (
<div className="App">
<h1>n:{n}</h1>
<button onClick={() => setN(n + 1)}>onAddN</button>
<h1>m:{m}</h1>
<button onClick={() => setM(m + 1)}>onAddM</button>
</div>
);
}

render();

useState数组方案缺陷

注意事项:外面不能加 if

因为上面的实现我们知道了,实际state是一个[],你加了 if的时候 ,这个 index索引就乱了

  • 第一次调用 n 是第一个,第二次调用 m 是第二个
  • 必须保证他们的顺序是一致的
  • 所以不能这样
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function App() {
console.log("app 运行了");
const [n, setN] = myUseState(0);
let m,setM
if(n%2 === 1){
[m, setM] = myUseState(0);
}
console.log("n:" + n);
return (
<div className="App">
<h1>n:{n}</h1>
<button onClick={() => setN(n + 1)}>onAddN</button>
<h1>m:{m}</h1>
<button onClick={() => setM(m + 1)}>onAddM</button>
</div>
);
}
  • vue3克服了这个问题

现在存在的问题

  • App 用了 _state 和 index,其他组件咋办

    • 解决办法给每个组件创建一个 _state 和 index
  • 放在全局变量重名了咋办?

    • 放在组件对应的 虚拟dom节点身上
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
开始的时候 render(<App/>)

生成虚拟dom
vnode = {
tag:'div',
type:'App'
_state:[],
index:0
}

第一次渲染的时候 触发 useState(0)
渲染 0
第二次 onAdd的时候
产生新的 vnode2 同时 测试的 n = n+1

diff算法 分析差异 ,patch打补丁,更新 <App/>
渲染更新后的 vnode2,n = 1

总结

  • 每个函数组件对应一个 React节点, 虚拟dom
  • 每个节点保存着 state和 index
    • _state 真实名称为 memoizedState
    • index的实现用到了 链表
  • useState 会读取 state[index]
  • index 由 useState 出现的次数 依次递增
  • setState 会修改 state,并触发更新

useState的错误理解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import React, { useState } from "react";
import ReactDOM from "react-dom";

function App() {
const [n, setN] = useState(0);
const log = () => {
setTimeout(() => {
console.log(`n:${n}`);
}, 3000);
};
return (
<div className="App">
<h1>{n}</h1>
<button onClick={() => setN(n + 1)}>+1</button>
<button onClick={log}>log</button>
</div>
);
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

先 log 在 +1

  • 打印 0

先+1 在 log

  • 打印 n+1

错误理解就是:setN会改变n

  • setN 不会改变 n
  • 每次+1都会有一个新的n
  • n有分身

能不能一个 n贯穿始终

  • 推荐你用 vue3
  • 因为react推行 函数式,每次都是一个迭代,原来的已经不复存在了

贯穿始终

  • 全局变量
    • window.xxx 即可 太low
  • useRef
    • useRef不仅可用于 div,还能用于 任意数据
    • 但是 n+1 后 ,不会触发UI更新,我就想让它更新咋办? 用vue3
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      import React from "react";
      import ReactDOM from "react-dom";

      function App() {
      const nRef = React.useRef(0)
      const log = () => {
      setTimeout(() => {
      console.log(`n:${nRef.current}`);
      }, 3000);
      };
      return (
      <div className="App">
      <h1>{nRef.current}</h1>
      <button onClick={()=>(nRef.current+=1)}>+1</button>
      <button onClick={log}>log</button>
      </div>
      );
      }

      const rootElement = document.getElementById("root");
      ReactDOM.render(<App />, rootElement);

变通的触发UI更新,只要你不觉得自己蛋疼

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
import React from "react";
import ReactDOM from "react-dom";

function App() {
const nRef = React.useRef(0)
// 通过 跟初始值不同,来update
// 但是非常的蛋疼这样写。还是用 vue3吧
const update = React.useState(null)[1]
const log = () => {
setTimeout(() => {
console.log(`n:${nRef.current}`);
}, 3000);
};
return (
<div className="App">
<h1>{nRef.current}</h1>
<button onClick={()=>{
nRef.current+=1;
update(nRef.current)
}}>+1</button>
<button onClick={log}>log</button>
</div>
);
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
  • vue3 的 ref 变更后会触发 UI更新,同时这个 ref贯穿始终

useContext来实现贯穿始终

  • 代码

  • 不管你组件嵌套多少层都可以

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
import React from "react";
import ReactDOM from "react-dom";
import "./styles.css";
const rootElement = document.getElementById("root");

const themeContext = React.createContext(null);

function App() {
const [theme, setTheme] = React.useState("red");
return (
<themeContext.Provider value={{ theme, setTheme }}>
<div className={`App ${theme}`}>
<p>{theme}</p>
<div>
<ChildA />
</div>
<div>
<ChildB />
</div>
</div>
</themeContext.Provider>
);
}

function ChildA() {
const { setTheme } = React.useContext(themeContext);
return (
<div>
<button onClick={() => setTheme("red")}>red</button>
</div>
);
}

function ChildB() {
const { setTheme } = React.useContext(themeContext);
return (
<div>
<button onClick={() => setTheme("blue")}>blue</button>
</div>
);
}

ReactDOM.render(<App />, rootElement);

ReactWheels07-01组件的两种写法Class和函数

Class组件写法

index.tsx

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
import React from "react";
import ReactDOM from "react-dom";

import "./styles.css";
interface Props {
message: string;
}
interface State {
n: number;
}
class App extends React.Component<Props, State> {
static defaultProps = {
message: "hello"
};
static displayName = "Hjx";
y: () => void;
constructor(props) {
super(props);
this.state = {
n: 1
};
this.y = this.x1.bind(this);
// 等价于 this.y = ()=> this.x1.bind(this)
}
x = () => {
this.setState({
n: this.state.n + 1
});

// 这种写法相当于 this.y = this.x1.bind(this);
};
x1() {
console.log(this);
//1 当前实例
//2 undefined
// 答案是 2
// react 不帮你绑定 this
// 你只能 onClick={this.x1.bind(this)}
}
render() {
return (
<div className="App">
<div>{this.props.message}</div>
<div>{this.state.n}</div>
<button onClick={this.x}>+1</button>
<br />
<button onClick={this.x1}>默认不绑定this</button>
<br />
<button onClick={this.x1.bind(this)}>bind(this)绑定this</button>
<br />
<button onClick={this.y}>内部声明的y函数</button>
</div>
);
}
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

理解

1
2
3
4
5
6
7
class App extends React.Component<Props, State> {

x() {
...
}
}
这样写相当于 x() 挂载在 原型上, 也就是多个实例共享一个 x()

推荐使用的方式

这种写法有缺陷,相当于 this.x = ()=>{} ,也就是每个实例有一个 x()

浪费内存

如何解决? 无解

1
2
3
4
5
x = () => {
this.setState({
n: this.state.n + 1
});
};

defaultProps

  • 为 props 设置默认值,就是当用户不传递 message的时候
1
2
3
static defaultProps = {
message: "hello"
};

displayName

1
static displayName = "Hjx";

如果我们的用户是 JS 呢?

  • 它绕过类型检查传递一个 message=1 数字1 如何处理
  • 你需要安装依赖 prop-types 用来检查 js 传参的类型
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
import React from "react";
import ReactDOM from "react-dom";
// 引入 prop-types
import PropTypes from "prop-types";
import "./styles.css";
interface Props {
message: string;
}
interface State {
n: number;
}
class App extends React.Component<Props, State> {
static defaultProps = {
message: "hello"
};
static displayName = "Hjx";
// 使用 prop-types 限制用户传递的 message 类型
static propTypes = {
message: PropTypes.string
};
y: () => void;
constructor(props) {
super(props);
this.state = {
n: 1
};
}

render() {
return (
<div className="App">
<div>{this.props.message}</div>
</div>
);
}
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

React 也有计算属性

index.tsx

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
import React from "react";
import ReactDOM from "react-dom";
import "./styles.css";
interface Props {
message: string;
}
interface State {
firstName: string;
lastName: string;
}
class App extends React.Component<Props, State> {
static defaultProps = {
message: "hello"
};
// 除了可以用计算属性
get name() {
return this.state.firstName + " " + this.state.lastName;
}
// 还可以给计算属性 赋值
set name(newName) {
const [firstName, lastName] = newName.split(" ");
this.setState({
firstName,
lastName
});
}
constructor(props) {
super(props);
this.state = {
firstName: "Frank",
lastName: "Aaa"
};
}
render() {
return (
<div className="App">
<div>{this.props.message}</div>
<div>{this.name}</div>
</div>
);
}
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

函数组件

  • 代码
  • 注意!! 必须 React在 16.8及以上版本才能用 state

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import React from "react";
import ReactDOM from "react-dom";

import "./styles.css";
interface Props {
message: string;
}
const App: React.FunctionComponent<Props> = (props) => {
return (
<div className="App">
Hello
</div>
);
};

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

使用 state

  • 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    import React, { useState } from "react";
    import ReactDOM from "react-dom";

    import "./styles.css";
    interface Props {
    message: string;
    }
    const App: React.FunctionComponent<Props> = props => {
    const [n, setN] = useState(1);
    const x = () => {
    setN(n + 1);
    };
    return (
    <div className="App">
    <div>{props.message}</div>
    <div>{n}</div>
    <button onClick={x}>+1</button>
    </div>
    );
    };

    const rootElement = document.getElementById("root");
    ReactDOM.render(<App message="hello" />, rootElement);

完整的 函数组件

  • 代码
  • useState 来作 state.n 的初始化和设置

    1
    2
    3
    4
    5
    // useState(1) 初始化 n = 1;
    const [n, setN] = useState(1);
    const x = () => {
    setN(n + 1);
    };
  • useEffect 来监听 state 改变 第二个参数为[监听的state]

  • useEffect 做 once 操作 第二个参数为[] mounted操作
  • useEffect 做组件销毁的处理, return 里写处理
  • prop-types 做js参数类型检查
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
import React, { useState, useEffect } from "react";
import ReactDOM from "react-dom";
import PropTypes from "prop-types";
import "./styles.css";
interface Props {
// message 可有可无 此时有个坑
// 你在 函数组件里 props.message.split(' ')
// 报错不通过
message?: string;
}
const App: React.FunctionComponent<Props> = props => {
const [n, setN] = useState(1);
const [m, setM] = useState(2);
const x = () => {
setN(n + 1);
};
const y = () => {
setM(m + 1);
};
// 在每次UI更新之后执行 mounted或 updated之后
/*
useEffect(() => {
console.log("UI更新之后,mounted或 updated之后");
});
*/
// 单独监听 n 的改变
/*
useEffect(() => {
console.log("UI更新之后,n");
}, [n]);
*/
useEffect(() => {
console.log("只执行一次");
// 组件死之前想做什么 就 return
return () => console.log("我死了");
}, []);
// 因为 你定义的 message?:string 代表可有可无
// props.message.split(" ");
// 你只能用以下两种方式 强制断言
(props.message as string).split(" ");
props.message!.split(" ");
return (
<div className="App">
<div>{props.message}</div>
<div>{n}</div>
<button onClick={x}>n+1</button>
<div>{m}</div>
<button onClick={y}>m+1</button>
</div>
);
};

App.defaultProps = {
message: "default message"
};
App.displayName = "Hjx";
App.propTypes = {
message: PropTypes.string
};
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

两种组件方式的区别

  • 16.8.0及以上 才可以使用 useState / useEffect API 也就是 hooks API
  • 16.8.0以前的函数组件为 无状态组件
  • 16.8.0以后的函数组件可以代替 class组件

解决 props可有可无

使用解构赋值,但是这样代码不好理解

1
2
3
4
const App: React.FunctionComponent<Props> = (
{message = 'default message'}) => {
props.message.split(" ");
};

函数组件和class组件区别

函数 vs
无状态(useState) 有状态(state)
可以实现函数式 很难实现函数式
setN执行之后原来的状态[n,setN]等都不要了,会得到一个新的[n,setN] this.setState({n:1}) ,n一直在this.state.n
迭代,每次得到一个最新的状态,之前的全部不要了 在一个对象上不停的进行赋值,修改的都是同一个对象

函数式的本质就是 不能进行第二次赋值

  • 函数式好处就是 可以使用数学知识
  • 面向对象的好处是 适合人类
1
2
3
4
a = 1
a = 2 就不行

而面向对象可以

ZB-021-Collection体系原理

Collection体系简介

  • Collection 是接口,无法实例化
  • 有非常非常多的类继承它
  • 它是整个Collection的根接口

最重要的两个实现(接口)

  • List
  • Set

list添加元素的三种方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Main {
public static void main(String[] args) {
Collection<Integer> c = new LinkedList<Integer>();

List<Integer> list = new ArrayList(c);

List<Integer> list2 = new ArrayList();
list2.addAll(c);


List<Integer> list3 = new ArrayList();
for (Integer i: c) {
list3.add(i);
}

}
}

Collection常用方法

  • new: new ArrayList(Collection) 或者 new ArrayList()
  • R:size()/isEmpty()/contains()/for()/stream()
  • C/U:add()/addAll()/retainAll()

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // 求交集
    List<Integer> list = new ArrayList();
    list.add(1);
    list.add(2);

    List<Integer> list2 = new ArrayList();
    list2.add(2);
    list2.add(3);


    list.retainAll(list2);
    System.out.println(list); // [2]
  • D:clear()/remove()/removeAll()

List

  • 最常用的ArrayList
    本质就是一个数组
  • 面试题:动态扩容的实现
    • 创建一个更大的空间,然后把原先的所有元素拷贝过去
  • add()方法

ArrayList扩容源代码

  • 非常非常好的代码风格
  • 代码清晰易懂,命名非常好(见名知意)
  • 傻子都可以写出让机器识别的代码,但优秀的程序员应该写出人类轻松识别的代码
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
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{

...

public boolean add(E e) {
// 确保容量够
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}

private void ensureCapacityInternal(int minCapacity) {
// 计算容量
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

private void ensureExplicitCapacity(int minCapacity) {
modCount++;

// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}

private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
// 右移一位就是 除2
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}


private static int calculateCapacity(Object[] elementData, int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
...
}

疑问?扩容后,之前的数组是不是变成垃圾了

  • 垃圾回收帮你处理,你不用操心,你操心也操心不上。不归你管

Set

1
2
3
4
5
6
7
8
9
10
11
12
public class Main {
public static void main(String[] args) {

List<Integer> list = new ArrayList();
list.add(1);
list.add(2);
list.add(2);
list.add(3);

Set<Integer> set = new HashSet(list); // [1,2,3]
}
}
  • equals约定:不允许有重复元素
    判断重复:equals
  • 如果你有一个Set,如何实现?

    • 容易想到的办法都比较低效
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      class MySet {
      List<Object> elements;

      void add(Object object){
      if(!elements.contains(object)){
      elements.add(object);
      }
      }
      }

      // 每次添加的时候都要比较当前列表里是否存在,非常的低效
      // 假设Set有100万数据,每次都比较100万次 非常低效
      set ==> [张三,李四,王五]

      set.add("张三");
      挨个比较非常低效
  • Java世界里第二最大重要的约定:hashCode

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    改良上面的低效问题, 百家姓
    张:张三,张三丰
    李:李四
    赵:赵六

    这样每次添加的时候 如 添加 赵奕欢
    先找 “赵”
    这样无形中提高了效率

    这个“百家姓”就是 hashCode

    同理我们想知道我们的 Object是不是重复的
    - 新建很多“桶” ==>哈希桶
    - 映射成一个 int值 均匀的分配到 桶里面去
    - 此时有一个 obj 来了,想知道它存在不存在,只需要算一下它的 hashCode
    - 然后去对应的 hash桶里找就好了
    • 同一个对象必须始终返回相同的hashCode
    • 两个对象的 equals 返回 true,必须返回相同的 hashCode
    • 两个对象不等,也可能返回相同的hashCode
      • 百家姓里 “张” 姓 张三,张三丰 都姓“张” ==> hashCode相同

哈希算法

  • 哈希就是一个单向的映射
  • 例子:从姓名到姓的哈希运算
  • 从任意对象到一个整数的hashCode

注意! 整数int的值是有范围的 正副21亿,而对象是无限多的可能

HashSet

  • 最常用!最高效的Set实现

    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
    public class Main {
    public static void main(String[] args) {


    List<Integer> list = new ArrayList();
    Set<Integer> set = new HashSet();

    for (int i = 0; i < 1000_0000; i++) {
    set.add(i);
    list.add(i);
    }

    long t0 = System.nanoTime();
    set.contains(9999_9999);

    long t1 = System.nanoTime();
    list.contains(9999_9999);

    long t2 = System.nanoTime();

    System.out.println("set:"+(t1-t0)/1000.0/1000);
    System.out.println("list:"+(t2-t1)/1000.0/1000);
    }
    }

    // 执行效率
    set:0.016948
    list:29.803517
  • HashSet 是无序的!如果有需要使用LinkedHashSet(双向链表)

Map

  • C/U:put/putAll
  • R
    • get/size
    • containsKey/containsValue
    • keySet/values/entrySet
  • D:remove/clear

Map非常容易踩到的坑

1
2
3
4
5
6
7
8
9
10
11
Map<String,String> map = new HashMap<>();
map.put("a":"1");
map.put("b":"2");

Set<String> keyset = map.keySet(); // ["a","b"]

// 坑来了
// 你修改 map 里的 key 会映射到 keyset里
// 你修改 keyset 里的值 会映射到 map里
map.remove("a");
// 此时 keyset ==》["b"]

entrySet

1
2
3
4
5
6
7
Map<String,String> map = new HashMap<>();
map.put("a":"1");
map.put("b":"2");
for(Map.Entry<String,String> entry:map.entrySet()){
System.out.println(entry.getKey());
System.out.println(entry.getValue());
}

面试题

HashMap 和 HashSet 区别

  • HashMap的key的Set就是一个 HashSet

请说一下 HashMap 扩容过程

  • 跟 ArrayList 一样,创建一个更大的 HashMap
  • 然后把之前的元素拷贝过来

问到唬住就50K唬不住就5K

HashMap的线程不安全性

HashMap在 Java7+ 后的改变: 链表==> 红黑树

  • 我们知道 HashSet/HashMap 本质实现是有很多 哈希桶 存储相同 hashCode
  • 但是之前说到 桶的个数是有限的,而对象可能是无限的 此时可能发生碰撞
  • 以致于 hashCode相同 因此被放到同一个 Hash桶里 ,但是 它们是不同对象所以equals 返回false
  • 问题来了,假如一组数据 它们hashCode都一样。你会发现 HashSet 就退化成了一个 链表
  • 假如你有100万个元素 你希望平均的分配在10万个桶里,但如遇到碰巧精心设计的一组数据 hashCode 相同,此时这个桶的性能就会急剧恶化
  • 坏处就是 HashSet 的性能和 List 一样了

因此在JDK7之后,当发生 哈希碰撞的时候,这个东西就不是链表了而是 红黑树

  • 红黑树 超级复杂的数据结构
    • 如被问到知道是什么东西就行了
    • 原理都不用知道
    • 面试官也够呛知道,如果让你手写,就说我不会,等面试结束的反问问题,我不会红黑树,你给我写一个红黑树!

补充知识

Set

  • HashSet 完全随机
  • LinkedHashSet 保证和插入顺序一样
  • TreeSet 有序的(会进行一个排序) 自然顺序从小到大
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Main {
public static void main(String[] args) {

List<Integer> list = Arrays.asList(100000,142,-2,-323121,321321,15);

Set set1 = new HashSet(list);
Set set2 = new LinkedHashSet(list);
Set set3 = new TreeSet(list);

set1.forEach(System.out::println); // 无序的
set2.forEach(System.out::println); // 按添加顺序
set3.forEach(System.out::println); // 排好序的
}
}

TreeSet / ArrayList查找时间复杂度

  • ArrayList O(n)
  • TreeSet O(log n)

把算法的复杂度从 线性时间下降到对数时间

TreeSet/TreeMap

  • 二叉树/红黑树
  • 使用Comparable约定,认定排序相等的元素相等
  • 二叉树查找/插入

Collections 工具方法集合

  • emptySet() 返回一个空的集合
  • synchronizedCollection: 将一个集合变成线程安全的
  • unmodifiableCollection: 将一个集合变成不可变的
    1
    2
    3
    4
    5
    List<Integer> list = Arrays.asList(100000,142,-2,-323121,321321,15);
    Set set1 = new HashSet(list);
    Set unmodifiableSet = Collections.unmodifiableSet(set1);

    unmodifiableSet.add(1); //报错

Collection 其他实现

  • Queue 队列/Deque 双端队列
    • 任何你想用 Stack的时候都可以使用 Deque
  • Vector/Stack
  • LinkedList 链表
  • ConcurrentHashMap
  • PriorityQueue 优先级队列 比如你的 闹钟列表 离你最近的那个会响

Guava 番石榴

补充java中 不完整的Collection实现

  • Lists/Sets/Maps
  • ImmutableMap/ImmutableSet 不可变的Map/Set
  • MultiSet/MultiMap

    1
    2
    3
    4
    5
    6
    7
    8
    9
    Set => 1,2,3,4

    #除了告诉你唯一的元素有那些,还可以告诉你 3 被加了几次
    MultiSet => 1,2,3x3,4

    Map key:value

    MultiMap
    key:对应多个value
  • BiMap 双向Map

    • 普通的Map是 value对key的映射
    • BiMap 是双向映射

ZB-020-java包管理和maven

什么是包

  • JVM的⼯作被设计地相当简单:
    1. 执⾏⼀个类的字节码
    2. 假如这个过程中碰到了新的类,加载它,然后回到第一步,这样循环往复

那么,去哪⾥加载这些类呢?

  • 答案是 classpath类路径

类路径(Classpath)

  • 在哪⾥可以找到类
    • -classpath/-cp
  • 类的全限定类名(⽬录层级)唯⼀确定了⼀个类
  • 包就是把许多类放在⼀起打的压缩包 jar 包

每次 IDEA 运行java项目显示的命令行

程序运行时:会挨个在 classpath路径里找。

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
/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/bin/java "-javaagent:/Applications/IntelliJ IDEA.app/Contents/lib/idea_rt.jar=51272:
/Applications/IntelliJ IDEA.app/Contents/bin"
-Dfile.encoding=UTF-8
-classpath
/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/jre/lib/charsets.jar:
/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/jre/lib/deploy.jar:
/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/jre/lib/ext/cldrdata.jar:
/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/jre/lib/ext/dnsns.jar:
/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/jre/lib/ext/jaccess.jar:
/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/jre/lib/ext/jfxrt.jar:
/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/jre/lib/ext/localedata.jar:
/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/jre/lib/ext/nashorn.jar:
/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/jre/lib/ext/sunec.jar:
/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/jre/lib/ext/sunjce_provider.jar:
/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/jre/lib/ext/sunpkcs11.jar:
/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/jre/lib/ext/zipfs.jar:
/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/jre/lib/javaws.jar:
/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/jre/lib/jce.jar:
/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/jre/lib/jfr.jar:
/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/jre/lib/jfxswt.jar:
/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/jre/lib/jsse.jar:
/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/jre/lib/management-agent.jar:
/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/jre/lib/plugin.jar:
/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/jre/lib/resources.jar:
/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/jre/lib/rt.jar:
/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/lib/ant-javafx.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/lib/dt.jar:
/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/lib/javafx-mx.jar:
/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/lib/jconsole.jar:
/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/lib/packager.jar:
/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/lib/sa-jdi.jar:
/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/lib/tools.jar:


# 当前目录的 target/classes 就是本项目里的 类文件

/Users/huangjiaxi/Desktop/java-fork-process/target/classes

com.github.hcsp.shell.Fork

什么是 jar 文件

  • 实际就是 跟zip一样压缩后的文件,只不过这里叫 jar包
每当 JVM 需要一个类时,它就会在当前的 classpath里 按照这个类的全限定类名挨个找,如果是jar包就解压缩然后在解压缩后的目录里找,如果是一个文件夹就直接按照目录找,找不到就抛出 classNotFound

如果你依赖的类中还依赖了别的包呢?

1
2
A 依赖 B , B 依赖 C , C 又依赖 D
D 从那里找呢?

这就是 传递性依赖 ,这就是为什么上面的命令行里 classpach 那么长的原因,要把所有依赖的path 添加进来

一个简单的项目命令行尚且如此之长,而一个真实项目可能有成百上千的依赖,如果你手动拼接,肯定又各种各样的问题,这就是为什么使用自动化工具的原因。

自动化的本质就是:帮我们把一些很啰嗦很累赘变成自动化

别急,还没完

没有maven时,你要手动导入jar包还有相关依赖,多人协作的时候非常麻烦。

  • 传递性依赖
    • 你依赖的类还依赖了别的类
  • Classpath hell
    • 全限定类名是类的唯⼀标识
    • 当多个同名类同时出现在Classpath中,就是噩梦的开始

当你的 classpath里包含了相同包,但是版本不同会发生什么

1
2
3
-classpath 
xxx.5.0.0.jar:
xxx.4.0.0.jar
  • 此时两个jar包里 有同名类 a
  • 在你程序运行时 会引用那个 类?

答案是 谁在前面用谁

问题升级,引入版本不同,但是同名的jar包

  • 版本不同的时候,代码可能是不一样的,以致于你的程序看上去可以运行。突然三天或三年后的某一天突然运行到一个地方出错了。你就很懵
    1
    2
    3
    4
    5
    6
    7
    8
    9
    -classpath 
    xxx.4.0.0.jar:
    xxx.5.0.0.jar

    # 4.0.0 是你实际使用的jar包
    # 5.0.0 是更新后的一个大版本修复了4.0.0的某些bug, 可能一些方法 API 已经变的面目全非了。
    但是在你看在程序还是正常的跑,只是没运行到出bug的地方
    你的项目如期上线,然后某天爆出个bug
    这个情况就叫 Classpath hell 依赖地狱

什么是包管理

  • 你要使⽤⼀些第三⽅类,总要告诉JVM从哪⾥找吧?
  • 包管理的本质就是告诉JVM如何找到所需的第三⽅类库
  • 以及成功地解决其中的冲突问题

没有Maven的蛮荒年代

以前怎么做的这些依赖

黑暗时代

  • ⼿动写命令进⾏编译运⾏
1
2
3
4
javac -cp  xx.jar xx2.jar Main.java
java -cp xx.jar xx2.jar Main

# 假设此时 jar包成百上千呢?

启蒙时代

  • Apache Ant
    • ⼿动下载jar包,放在⼀个⽬录中
    • 写XML配置,指定编译的源代码⽬录、依赖的jar包、输出
      ⽬录等
  • 缺点
    • 每个⼈都要⾃⼰造⼀套轮⼦
      1. 我的放在 libs
      2. 你的放在 sources
      3. 他的放在 libaray
    • 依赖的第三⽅类库都需要⼿动下载,费时费⼒
      • 假如你的应⽤依赖了⼀万个第三⽅的类库呢?都需要把依赖的库挨个下载下来
    • 没有解决Classpath地狱的问题,碰上重复的库你就完蛋了

Maven——划时代的包管理

  • Convention over configuration
  • 约定优于配置
  • 必须强调,Maven远远不⽌是包管理⼯具
  • Maven的中央仓库
    • 按照⼀定的约定存储包(坐标)
  • Maven的本地仓库
    • 默认位于~/.m2
    • 下载的第三⽅包放在这⾥进⾏缓存
  • Maven的包
    • 按照约定为所有的包编号,⽅便检索
    • groupId/artifactId/version
      • 扩展:语义化版本
    • SNAPSHOT快照版本

当你不知道一个包的坐标你就搜

1
2
3
4
5
6
7
8
xxx.xxx.xxx maven 通常第一个就是包的正确的名字

每个包有自己的版本
如 junit-jupiter-api 5.5.0
它会不停的升级。版本一旦发布,maven的约定是不能更改只能继续升级。
目的是 maven 想要实现一个可重现的构建,就是你机子上跑的和我机子上跑的 是一样的。

因为一旦同版本允许修改,就可能你跑的时候是好的,我跑的时候是坏的

maven 为什么知道包从哪里下载呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
当你引入依赖

<!-- https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-api -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.4.2</version>
<scope>test</scope>
</dependency>


maven 就会去它的主仓库 https://repo1.maven.org/maven2/
按照你的 groupId 里的路径
https://repo1.maven.org/maven2/org
https://repo1.maven.org/maven2/org/junit/
https://repo1.maven.org/maven2/org/junit/jupiter

然后按照 artifactId 找路径
https://repo1.maven.org/maven2/org/junit/jupiter/junit-jupiter-engine/

然后按照版本号 5.4.2
https://repo1.maven.org/maven2/org/junit/jupiter/junit-jupiter-engine/5.4.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
junit-jupiter-engine-5.4.2-javadoc.jar            2019-04-07 17:29    683506      
junit-jupiter-engine-5.4.2-javadoc.jar.asc 2019-04-07 17:29 821
junit-jupiter-engine-5.4.2-javadoc.jar.asc.md... 2019-04-07 17:29 32
junit-jupiter-engine-5.4.2-javadoc.jar.asc.sh... 2019-04-07 17:29 40
junit-jupiter-engine-5.4.2-javadoc.jar.md5 2019-04-07 17:29 32
junit-jupiter-engine-5.4.2-javadoc.jar.sha1 2019-04-07 17:29 40
# 源代码
junit-jupiter-engine-5.4.2-sources.jar 2019-04-07 17:29 110900
junit-jupiter-engine-5.4.2-sources.jar.asc 2019-04-07 17:29 821
junit-jupiter-engine-5.4.2-sources.jar.asc.md... 2019-04-07 17:29 32
junit-jupiter-engine-5.4.2-sources.jar.asc.sh... 2019-04-07 17:29 40
junit-jupiter-engine-5.4.2-sources.jar.md5 2019-04-07 17:29 32
junit-jupiter-engine-5.4.2-sources.jar.sha1 2019-04-07 17:29 40
# 真正的 jar包
junit-jupiter-engine-5.4.2.jar 2019-04-07 17:29 177798
junit-jupiter-engine-5.4.2.jar.asc 2019-04-07 17:29 821
junit-jupiter-engine-5.4.2.jar.asc.md5 2019-04-07 17:29 32
junit-jupiter-engine-5.4.2.jar.asc.sha1 2019-04-07 17:29 40
junit-jupiter-engine-5.4.2.jar.md5 2019-04-07 17:29 32
junit-jupiter-engine-5.4.2.jar.sha1 2019-04-07 17:29 40
# 当前包所依赖的包
junit-jupiter-engine-5.4.2.pom 2019-04-07 17:29 2395
junit-jupiter-engine-5.4.2.pom.asc 2019-04-07 17:29 821
junit-jupiter-engine-5.4.2.pom.asc.md5 2019-04-07 17:29 32
junit-jupiter-engine-5.4.2.pom.asc.sha1 2019-04-07 17:29 40
junit-jupiter-engine-5.4.2.pom.md5 2019-04-07 17:29 32
junit-jupiter-engine-5.4.2.pom.sha1 2019-04-07 17:29 40

这样就实现了。你pom引入了一个junit-jupiter-engine-5.4.2的依赖,然后它下载后根据junit-jupiter-engine-5.4.2.pom文件去下载 当前包的依赖(传递性依赖)

这样就形成了一颗依赖树,你可以在 IDEA maven工具栏的Dependencies 里看到

pom.xml里的aliyun镜像,就是为了下载更快。也可以配置到你本地安装的maven里

1
2
3
4
5
6
7
<repositories>
<repository>
<id>alimaven</id>
<name>aliyun maven</name>
<url>http://maven.aliyun.com/nexus/content/groups/public/</url>
</repository>
</repositories>

Maven的包

  • 按照约定为所有的包编号,⽅便检索
  • groupId/artifactId/version
  • SNAPSHOT快照版本
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    https://repo1.maven.org/maven2/org/junit/jupiter/junit-jupiter-engine/

    你开发的包可能不会立马上线,需要一直测一直改一直发布。但是总不能每次都发布一个版本吧!
    5.0.0/ 2017-09-10 18:09 -
    5.0.0-M1/ 2016-07-07 09:05 -
    5.0.0-M2/ 2016-07-23 18:14 -
    5.0.0-M3/ 2016-11-30 09:06 -
    5.0.0-M4/ 2017-04-01 19:23 -
    5.0.0-M5/ 2017-07-04 16:39 -
    5.0.0-M6/ 2017-07-18 19:27 -
    # RC叫做 发布准备版 但是它们都不是 SNAPSHOT快照版本,也基本不会在中央仓库里放置
    # 中央仓库都是放成熟的。
    5.0.0-RC1/ 2017-07-30 19:13 -
    5.0.0-RC2/ 2017-07-30 20:37 -

maven 默认会帮你下载字节码的包,如需看源码 可以在 IDEA 点击下载

包冲突

当你看到如下的异常的时候,包冲突发生了

  • AbstractMethodError
  • NoClassDefFoundError
  • ClassNotFoundException
  • LinkageError

举个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
你的项目里
依赖了 A
依赖了 D

A 又依赖了 B , B又依赖了 C
D 又依赖了 C

|A
--|B
--|--|C 1.0

|D
--|C 2.0

此时 两个 C 就导致了 包冲突

-classpath A:B:C1.0:D:C2.0

# 因为之前说了 classpath hell 依赖地狱
由于全限定类名是类的唯一标识,此时如果存在两个同名不同版本的库 同时出现在 classpath 中 那么噩梦开始了

如果有同名的库出现在 classpath 中 JVM 会选择靠前的那个 也就是 C1.0

但是 靠前的那个 C1.0 不一定是你想要的那个,有可能只是你忘了。

对此 maven 的原则是

  • 传递性依赖的⾃动管理
    • 原则:绝对不允许最终的classpath出现同名不同版本的jar包
  • 依赖冲突的解决:原则:最近的胜出

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    你的项目里
    依赖了 A
    依赖了 D

    A 又依赖了 B , B又依赖了 C
    D 又依赖了 C

    |A
    --|B
    --|--|C 1.0

    |D
    --|C 2.0

    C2.0 更加离 依赖树的根更近
    也就是 C2.0 获胜,淘汰 C1.0
    -classpath A:B:D:C2.0
    • 这个过程绝大多数是可以正常工作的,但是
    • mvn dependency:tree
  • 依赖的scope

实战:解决包冲突

1
2
3
4
5
6
7
8
9
10
11
12
13
14
你的项目依赖了 A / D

|A 0.1
--|B 0.1
--|--|C 0.2


|D 0.1
--|C 0.1

此时 maven 帮你淘汰了 C0.2 版本 因为它离得太远
-classpath A0.1:D0.1:B0.1:C0.1

此时就有 bug ,因为你想要的是 C0.2

为什么会干掉 C0.2,而不是C0.1

  • 答案是 maven就是这样设计的,它不分析语义版本,对于人类我们知道 0.2>0.1,但是maven在设计上的原则是谁离得更近,Gradle的策略是选择版本高的,但这也不是万能的。
  • 很多时候还是要人工介入

解决方案

  1. 强行引入 C0.2版本

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    |A 0.1
    --|B 0.1
    --|--|C 0.2

    |D 0.1
    --|C 0.1

    |C0.2

    C0.2 离得比 C0.1近。所以淘汰 C0.1
  2. exclusion 排除指定依赖

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    |A 0.1
    --|B 0.1
    --|--|C 0.2

    |D 0.1
    --|C 0.1
    通过 exclusion 来告诉 maven 忽略某个依赖 的子依赖
    <dependency>
    <groupId>xxx.xxx</groupId>
    <artifactId>D</artifactId>
    <version>0.1</version>
    <exclusions>
    <groupId>yyy.yyyy</groupId>
    <artifactId>C</artifactId>
    <version>0.1</version>
    </exclusions>
    </dependency>
  3. 一个IDEA 插件 maven helper

    • 能帮你分析当前的依赖树 和被淘汰的依赖标红显示
    • 你想排除指定版本只需要选中依赖 点击 Exclude
    • 一目了然帮你看到依赖树

如下依赖谁会被淘汰

1
2
3
4
5
6
7
8
9
|A 
--|B
--|--|C0.1

|D
--|E
----|C0.2

C0.1 获胜,因为C0.1 路径相同则选择靠前的那个

依赖的 scope

实现代码的隔离

  • <scope>test</scope> 代表只在 src/test目录可见
  • <scope>compile</scope> 在 src/main 和 src/test 都有效
  • <scope>provided</scope> 只在编译的时候有效
    1. 我们运行java的时候 右键 run 会先编译,此时会把依赖包在 -classpath 里体现
    2. 而在运行的时候 -classpath 不包含依赖包的路径
    3. 它的意思就是 编译的时候需要,运行的时候别人帮我提供,你不需要帮我加,防止包冲突。 最经典的就是 tomcat ,你的项目编译后会放在 tomcat容器里,tomcat 会帮你提供这些依赖包
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      <dependency>
      <groupId>org.junit.jupiter</groupId>
      <artifactId>junit-jupiter-api</artifactId>
      <version>5.4.2</version>
      <scope>test</scope>
      </dependency>
      <dependency>
      <groupId>org.junit.jupiter</groupId>
      <artifactId>junit-jupiter-engine</artifactId>
      <version>5.4.2</version>
      <scope>compile</scope>
      </dependency>

maven 不仅仅是包管理工具还是一个自动化构建工具

推荐一本书 Maven实战 google搜 Maven实战 site:github.com

  • Maven实战哪些章节是值得看的?
    • 1 简介可以看看
    • 3.1-3.4 代码是过时的
    • 4 可以看 背景案例也是过时的 看场景
    • 5
    • 6
    • 7
    • 8
    • 其他选看
  • Maven项⽬的基本结构(传世经典)
  • 基本概念:坐标和依赖/⽣命周期/仓库/聚合和继承
  • 使⽤Maven进⾏测试
  • 如果需要开发插件的话:
  • Maven的插件

真实世界中的Maven

  • 分析若⼲真实世界的Maven仓库
  • pom的含义 project object model 项目说明书

练习

  1. 修复commons-lang项目的pom文件
    我的pr
  2. 解决包冲突
    我的pr
  3. 实现语义化版本
    我的pr

ZB-016-命令行详解02

输⼊与输出

进程启动之后有三个要素,它们不和任何编程语言绑定,而且任何编程语言里都有这样的概念

  • 标准输⼊ stdin
  • 标准输出 stdout
  • 标准错误 stderr
1
2
3
4
5
6
7
8
public class Main{
public static void main(String[] args){
// 标准输出
System.out.println("output")
// 标准错误
System.err.println("error")
}
}

执行我们的 java

1
2
3
4
javac Main.java && java Main
输出
output
error

在操作系统中

  • stdout 叫做 “文件描述符1”
  • stderr 叫做 “文件描述符2”

将输出放到文件里

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java Main > output.txt
# 发现少了一行,因为输出默认会把标准输出重定向到文件里
> error

# 一个 “>” 代表重写覆盖, 两个">>"代表追加

重定向的 ">" 默认等价于 "文件描述符1"

# 执行 文件描述符2
java Main 2> output.txt
# error 写在了 文件里
> output

# 如何把 标准输出和 标准错误 同时输出到一个文件里
java Main > output 2>&1

# 顺序写反是无效的
java Main > output 1>&2
error

# 现在会同时打印 标准输出和标准错误, 此时我只想要 标准输出,不要标准错误
java Main 2> /dev/null
# 意思是把 错误输出丢到垃圾桶里
  • 输出的重定向
    • 覆盖⽂件
    • 追加⽂件
    • 改变流向
    • /dev/null

Linux常⽤命令

  • 进⼊⽬录 cd (change directory)
  • 展示⽂件 ls/ll/ls -alth (list)
  • 移动/重命名⽂件 mv (move)
  • 删除⽂件 rm/rm -r/rm -rf (remove)
  • 拷⻉⽂件 cp/cp -r (copy)
  • 显示变量 echo
  • 导出变量 export
  • Git系列操作 git pull/push/add/commit/…

Linux常⽤命令

  • 新建⽬录 mkdir/mkdir -p (make directory)
  • 当前⽬录 pwd (print working directory)
  • 显示⽂件 cat (concatenate and print files)
  • 编辑⽂件 vi

怎么才能掌握命令行?

以下方式逐级晋升

  • ⾃⼰对着书敲命令
  • 尽可能地在开发中使⽤命令⾏
  • vi/git/mkdir, etc.
  • 使⽤命令⾏的各种⼩⼯具
  • 开发命令⾏的程序
  • ⾃⾏在云服务器上开发部署博客
  • ⾃⾏在云服务器上进⾏持续集成实战
  • ⽇常使⽤Linux进⾏开发

使用命令编译java程序

1
2
3
4
5
6
7
// 环境变量 
System.getenv("aaa")

// 系统属性 只在 jvm 中有效
System.getProperty("xxx")
System.getProperty("java.version")
System.getProperty("user.dir")

Main.java

1
2
3
4
5
6
7
8
9
10
11
12
public class Main{
public static void main(String[] args){
System.out.println("args:"+ java.util.Arrays.toString(args));

System.out.println("env:"+System.getenv("AAA"));

System.out.println("java version:"+System.getProperty("java.version"));
// System.getProperty("user.dir")

System.out.println("system property:"+System.getProperty("BBB"));
}
}

运行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
javac Main.java && java Main 1 2 3

args:[1,2,3]
env: null
java version:1.8.0...
system property:null


# 传递 系统属性
export AAA=123
java -DBBB=789 Main

args:[]
env: 123
java version:1.8.0...
system property:789
-D 的顺序有要求
1
2
3
4
5
6
7
8
9
10
11
12
javac -DBBB=789 Main
args:[]
env: 123
java version:1.8.0...
system property:789


javac Main -DBBB=789
args:[-DBBB=789]
env: 123
java version:1.8.0...
system property:null
classpath应用

假如我们的Main.java 里 用了第三方包

1
2
3
4
5
6
7
8
9
10
11
java Main 报错 class not found

javac -classpath common-lang3-3.9.jar Main.java
java Main
// 报错,意思只在 common-lang3-3.9.jar 的目录里找不在当前目录找

还可以简写
java -cp common-lang3-3.9.jar Main

# 正确写法 除了在 common-lang3-3.9.jar 找还在 当前目录找
java -cp common-lang3-3.9.jar:. Main

在看IDEA里运行main的时候启动时的命令行

1
/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/bin/java "-javaagent:/Applications/IntelliJ IDEA.app/Contents/lib/idea_rt.jar=51272:/Applications/IntelliJ IDEA.app/Contents/bin" -Dfile.encoding=UTF-8 -classpath /Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/jre/lib/charsets.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/jre/lib/deploy.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/jre/lib/ext/cldrdata.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/jre/lib/ext/dnsns.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/jre/lib/ext/jaccess.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/jre/lib/ext/jfxrt.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/jre/lib/ext/localedata.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/jre/lib/ext/nashorn.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/jre/lib/ext/sunec.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/jre/lib/ext/sunjce_provider.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/jre/lib/ext/sunpkcs11.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/jre/lib/ext/zipfs.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/jre/lib/javaws.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/jre/lib/jce.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/jre/lib/jfr.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/jre/lib/jfxswt.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/jre/lib/jsse.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/jre/lib/management-agent.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/jre/lib/plugin.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/jre/lib/resources.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/jre/lib/rt.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/lib/ant-javafx.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/lib/dt.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/lib/javafx-mx.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/lib/jconsole.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/lib/packager.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/lib/sa-jdi.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/lib/tools.jar:/Users/huangjiaxi/Desktop/java-fork-process/target/classes com.github.hcsp.shell.Fork

java 中 fork进程

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
package com.github.hcsp.shell;

import java.io.File;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Map;

public class Fork {
public static void main(String[] args) throws Exception {
// 请在这里使用Java代码fork一个子进程,将fork的子进程的标准输出重定向到指定文件:工作目录下名为output.txt的文件
// 工作目录是项目目录下的working-directory目录(可以用getWorkingDir()方法得到这个目录对应的File对象)
// 传递的命令是sh run.sh
// 环境变量是AAA=123

// 1. 可执行程序 2。 参数
ProcessBuilder pb = new ProcessBuilder("sh","run.sh");
// 3。 工作路径
pb.directory(getWorkingDir());
// 4。 设置环境变量
Map<String,String> envs = pb.environment();
envs.put("AAA","123");

// 把系统输入输出 继承到当前的进程中来
// pb.inheritIO();

// 将输出内容重定向到文件
pb.redirectOutput(getOutputFile());

pb.start();

}

private static File getWorkingDir() {
Path projectDir = Paths.get(System.getProperty("user.dir"));
return projectDir.resolve("working-directory").toFile();
}

private static File getOutputFile() {
return new File(getWorkingDir(), "output.txt");
}
}

练习

  1. Linux命令
    我的pr
  2. java命令
    我的pr
  3. 使用Java代码fork子进程
    我的pr

ZB-016-命令行详解01

什么是命令行

什么是 Kernel

负责和计算机硬件交互的,负责CPU调度和进程相关的调度/IO之类的操作。操作系统最底层

我们如何操作内核呢?

Linux 通过命令行里(终端)来与内核进行交互

通过命令行操作内核的过程 叫做 Shell

  • Kernel(内核) 与 Shell(壳)
  • 广义的命令行一切通过字符终端控制计算机的方式
    • Windows cmd/PowerShell/Git bash
    • UNIX/Linux系列 sh/zsh/Terminal etc

为什么需要 Shell

  • 因为你不能直接和操作系统内核进行交互

为什么需要命令行

  • 因为你不得不用
    • 几乎所有的服务器都运行在 linux 上
  • 将工作自动化
    • 自动化是一切生产力的来源(提高生产力)
    • 你一台机子需要 点点点操作,10000台呢?
  • 相比GUI,命令行更容易开发和维护
    • bug少
  • 远程连接命令行占用资源远远低于GUI
  • 命令行上的开发者工具更丰富

命令行的历史和流派

  • UNIX家族
    • POSIX标准
    • macOS
    • Linux
    • Windows Subsystem for Linux
  • Windows
    • 奇葩

Windows 和 UNIX 系统的区别

  • UNIX遵循POSIX标准,只要遵循POSIX标准就可以以同样的方式工作
    • 默认命令行交互
    • GUI只是个扩展
    • 只有一个盘符
  • Windows
    • GUI主要
    • 命令行是扩展的(导致命令行在windows上是个残废)
    • 多个盘符 cdef

如何安装 windows 的 linux 子系统

  • 搜索 Windows Subsystem for Linux

命令的全部要素

  • 可执⾏程序(Executable)
  • 参数
  • 环境变量(Environment variable)
  • ⼯作⽬录(Working directory)
    • 启动命令的路径
    • 相对路径都是相对于这个路径
    • 在Java中的应⽤

以上四个要素相同,就可以完全地“重现”⼀个命令

你碰到的各种各样古怪的问题,原因⼀定是上述四个要素之⼀

可执行程序和参数

1
2
3
ls -al 
# ls就是可执行程序
# -al就是参数

工作目录

1
pwd 就是当前的目录

环境变量

  • 进程(Process)
    • 进程是计算机程序运⾏的最⼩单位
    • 独占⾃⼰的内存空间和⽂件资源
  • 每个进程都和⼀组变量相绑定

    • 传递不同的环境变量可以让程序表现出不同的⾏为

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      # 当前终端里
      export aa=1

      # 当前终端进入node环境
      node
      # 获取环境变量
      > process.env.aa
      # 输出 '1'
      '1'

      # 此时新开一个终端
      node
      > process.env.aa
      undefined
    • CLASSPATH/GOPATH

  • 在进程的fork过程中,环境变量可以被完全继承(fork 就是生进程的过程)
  • 所有的操作系统/编程语⾔都⽀持环境变量
  • 局部和全局的环境变量

进程是那里来的

  • 进程是它爹生的,最开始系统启动的时候只有一个进程,在 Linux 中叫做 init进程,它生了一堆孩子 桌面/终端
  • 在终端里输入 node 就导致 终端产生一个孩子 node进程
  • 这就形成了一棵树——进程树
  • 环境变量会一级一级的向下继承
  • 意思是 父进程里有一个环境变量 a=1 那么它的子进程也有这个 a=1

环境变量实战

  • 通过export/set设置环境变量,通过echo读取环境变量
  • 从Java/Go/Python/Node.js中读取环境变量
  • 向Docker容器传递环境变量

    1
    2
    # -e 代表环境变量
    docker run -it -e AAA=1 ubuntu
  • 快速传递⼀个环境变量

  • 使⽤环境变量控制程序的⾏为

java中读取环境变量

1
2
3
4
5
public class Main{
public static void main(String[] args){
System.out.println(System.getenv("aa"));
}
}

局部和全局的环境变量

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
# 只在当前终端里有效,新开 终端无效
export a = 1

# 只对当前的命令有效
a=1 node aa.js

# 全局的环境变量
# 编辑你的 ~/.bash_profile
vi ~/.bash_profile
# 输入如下内容
export aaa = 123456

# 然后新开终端输入
echo $aaa

# 你可能遇到,为什么刚改了现在没生效
# 你要 source 一下
source ~/.bash_profile


# 注意!!! bash_profile只对 bash有效
# 注意!!! bash_profile只对 bash有效
# 注意!!! bash_profile只对 bash有效

取决于你用的终端是 bash 还是 zsh

可执⾏程序

  • Windows:exe/bat/com
  • UNIX/Linux:x权限 - 可执⾏权限
  • 去哪⾥找程序?

    • Windows:Path环境变量 + 当前⽬录
    • UNIX/Linux:PATH环境变量
      1
      2
      3
      echo $PATH

      /Users/huangjiaxi/flutter/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/Users/huangjiaxi/Desktop/soft/apache-maven-3.6.1/bin
  • 在脚本的第⼀⾏指定解释器(shebang)

  • 别名(alias)

设置环境变量

1
2
3
4
5
6
7
# windows
set ABC=123
echo %ABC%

# linux
export ABC=123
echo $ABC

初学命令行的困扰

1
2
3
4
5
6
7
# 执行命令时,windows会在 PATH 和 当前目录里找,


# 而 linux 只会在 PATH 里找
# 假设当前目录有 main 脚本
# 直接 main 不会执行 因为 linux 只会在 PATH里找
./main 才执行

如何执行一个文本文件

echo.sh 内容如下

1
2
echo 123
echo 456

分配执行权限

1
2
3
4
chmod 777 echo.sh

# 执行你的脚本
./echo.sh

写一个 node脚本

echo.sh

1
2
console.log(123);
console.log(456);

但是操作系统不认识 node!!!

教操作系统认识node

echo.sh

1
2
3
#!/usr/bin/env node
console.log(123);
console.log(456);

shell的约定

1
2
3
4
5
6
7
8
shell中 “#” 代表注释

首行 代表指定当前的文件由谁来解释和执行
# 从当前的上下文环境中查找 node 命令
#!/usr/bin/env node

为什么不推荐写绝对路径,因为你不知道别人的node是不是安装在这个路径
#!/usr/local/bin/node

参数

  • UNIX参数的约定——然鹅Java并不⻦这个约定

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    ‘-’ 约定
    ls -alth
    # 约定,以一个 ‘-’ 开头的可以 -a -l -t -h 也可以合并为 -alth
    # 意思就是 '-' 后面跟一个字符

    '--' 约定 后面跟单词
    ls -a <==> ls --all
    git push -f <==> git push --force


    但是 java 不遵守这个
    java -version
  • 参数中包含空格或者字符串

    • 单引号
    • 双引号
  • 参数的展开

坑人的 单双引号

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 当前环境里没有 A 变量,运行正常
javap Main$A.class

# 此时
export A=123
javap Main$A.class 报错了
实际执行的是 javap Main123.class

# 默认情况下 “$” 是把命令展开的符号

# 通过单引号来包裹它,运行正常,除了可以让多个空格分开的字符串连接在一起,还可以告诉命令行 里面的内容全不要替换
javap 'Main$A.class'

# 换个 "双引号" 试试
javap "Main$A.class"

双引号会把 里面的变量展开 ==> javap Main$123.class

# 想给程序传递 单引号怎么办?
# 包双引号
./main "'I am a boy'"
# 转义
./main \'I am a boy\'

扩展的参数

1
2
3
4
5
6
7
8
ls *.java
此时会把所有 .java扩展名的文件罗列出来

# 如何想不被扩展原样执行呢?

ls '*.java'

# * 代表通配符 默认会展开,如果你不想它自动扩展 就包含 单引号

ZB-014-java接口和抽象类02

Comparable 接口

1
2
3
4
5
6
7
8
9
public interface Comparable<T> {
/**
* Compares this object with the specified object for order. Returns a
* negative integer, zero, or a positive integer as this object is less
* than, equal to, or greater than the specified object.
...
*/
public int compareTo(T o);
}
  • natural ordering 自然顺序(从小到大)
1
2
3
a < b 返回 -1
a > b 返回 1
a = b 返回 0

实战

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
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
package com.github.hcsp.polymorphism;

import java.io.IOException;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;

public class Point implements Comparable<Point>{

private final int x;
private final int y;
// 代表笛卡尔坐标系中的一个点
public Point(int x, int y) {
this.x = x;
this.y = y;
}

public int getX() {
return x;
}

public int getY() {
return y;
}

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

Point point = (Point) o;

if (x != point.x) {
return false;
}
return y == point.y;
}

@Override
public int hashCode() {
int result = x;
result = 31 * result + y;
return result;
}

@Override
public String toString() {
return String.format("(%d,%d)", x, y);
}

// 按照先x再y,从小到大的顺序排序
// 例如排序后的结果应该是 (-1, 1) (1, -1) (2, -1) (2, 0) (2, 1)
public static List<Point> sort(List<Point> points) {
Collections.sort(points);
return points;
}

public static void main(String[] args) throws IOException {
List<Point> points =
Arrays.asList(
new Point(2, 0),
new Point(-1, 1),
new Point(1, -1),
new Point(2, 1),
new Point(2, -1));
System.out.println(Point.sort(points));
}

@Override
public int compareTo(Point that) {
// 比较 this 和 that
if(this.x < that.x){
return -1;
}else if(this.x > that.x){
return 1;
}

// 运行到此说明 this.x = that.x
// 因此我要可以进行 y 的排序
if(this.y < that.y){
return -1;
}else if(this.y > that.y){
return 1;
}

return 0;
}
}
  • 一个坑的地方千万不要这样比较 return this.x - that.x;
    • 记得溢出吗?
    • 两个数相减,不一定是你想要的那个数,同理两数相加也是一个道理
  • Comparable 实际也是策略模式,由你来决定策略的各种条件
  • 参考练习
  • 我的pr

另一个坑,注意ComparableTreeSet中的坑

  • Set 它是一个 无重复元素的集合
  • 而你对TreeSet 排序时候也就是实现Comparable 接口的,而调用compareTo时候
    • 一旦 o1.id = o2.id 导致compareTo 返回 0 这样就会丢失元素
    • compareTo不要返回0,否则一定踩到坑
  • TreeSet排序丢失元素的bug
  • 我的pr
    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
    public class User implements Comparable<User> {
    /** 用户ID,数据库主键,全局唯一 */
    private final Integer id;

    /** 用户名 */
    private final String name;

    public User(Integer id, String name) {
    this.id = id;
    this.name = name;
    }

    public Integer getId() {
    return id;
    }

    public String getName() {
    return name;
    }

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

    User person = (User) o;

    return Objects.equals(id, person.id);
    }

    @Override
    public int hashCode() {
    return id != null ? id.hashCode() : 0;
    }

    /** 老板说让我按照用户名排序 */
    @Override
    public int compareTo(User o) {
    if(name == o.name){
    return id.compareTo(o.id);
    }
    return name.compareTo(o.name);
    }

    public static void main(String[] args) {
    List<User> users =
    Arrays.asList(
    new User(100, "b"),
    new User(10, "z"),
    new User(1, "a"),
    new User(2000, "a"));
    TreeSet<User> treeSet = new TreeSet<>(users);
    // 为什么这里的输出是3?试着修复其中的bug
    System.out.println(treeSet.size());
    }
    }

实战

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
62
63
package com.github.hcsp.polymorphism;

import java.util.ArrayList;
import java.util.List;
import java.util.function.Predicate;

public class User {
/** 用户ID,数据库主键,全局唯一 */
private final Integer id;

/** 用户名 */
private final String name;

public User(Integer id, String name) {
this.id = id;
this.name = name;
}

public Integer getId() {
return id;
}

public String getName() {
return name;
}

// 过滤ID为偶数的用户
public static List<User> filterUsersWithEvenId(List<User> users) {
List<User> results = new ArrayList<>();
for (User user : users) {
if (user.id % 2 == 0) {
results.add(user);
}
}
return results;
}

// 过滤姓张的用户
public static List<User> filterZhangUsers(List<User> users) {
List<User> results = new ArrayList<>();
for (User user : users) {
if (user.name.startsWith("张")) {
results.add(user);
}
}
return results;
}

// 过滤姓王的用户
public static List<User> filterWangUsers(List<User> users) {
List<User> results = new ArrayList<>();
for (User user : users) {
if (user.name.startsWith("王")) {
results.add(user);
}
}
return results;
}
// 你可以发现,在上面三个函数中包含大量的重复代码。
// 请尝试通过Predicate接口将上述代码抽取成一个公用的过滤器函数
// 并简化上面三个函数
public static List<User> filter(List<User> users, Predicate<User> predicate) {}
}

用策略模式 和 匿名内部类

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
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
public class User {
/** 用户ID,数据库主键,全局唯一 */
private final Integer id;

/** 用户名 */
private final String name;

public User(Integer id, String name) {
this.id = id;
this.name = name;
}

public Integer getId() {
return id;
}

public String getName() {
return name;
}


// 你可以发现,在上面三个函数中包含大量的重复代码。
// 请尝试通过Predicate接口将上述代码抽取成一个公用的过滤器函数
// 并简化上面三个函数
public static List<User> filter(List<User> users, 判断条件是否成立 条件) {
List<User> results = new ArrayList<>();
for (User user: users) {
if(条件.这个用户是否满足条件(user)){
results.add(user);
}
}
return results;
}


private interface 判断条件是否成立 {
boolean 这个用户是否满足条件(User user);
}

private static class 用户ID是偶数的条件 implements 判断条件是否成立{

@Override
public boolean 这个用户是否满足条件(User user) {
return user.id % 2 == 0;
}
}

public static void main(String[] args) {
List<User> res = filterUsersWithEvenId(Arrays.asList(new User(1,"a"),new User(2,"b")));
System.out.println(res);

List<User> res2 = filterZhangUsers(Arrays.asList(new User(1,"张三"),new User(2,"李四")));
System.out.println(res2);
}

// 过滤ID为偶数的用户(接口实现类的实例)
public static List<User> filterUsersWithEvenId(List<User> users) {
List<User> results = new ArrayList<>();
return filter(users,new 用户ID是偶数的条件());
}

// 过滤姓张的用户(匿名内部类)
public static List<User> filterZhangUsers(List<User> users) {
return filter(users, new 判断条件是否成立() {
@Override
public boolean 这个用户是否满足条件(User user) {
return user.name.startsWith("张");
}
});
}

// 过滤姓王的用户
public static List<User> filterWangUsers(List<User> users) {
List<User> results = new ArrayList<>();
for (User user : users) {
if (user.name.startsWith("王")) {
results.add(user);
}
}
return results;
}
}

Predicate

  • 判定
  • 接口内部只有一个方法,返回值是boolean
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
62
63
64
65
66
67
68
69
package com.github.hcsp.polymorphism;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;

public class User {
/** 用户ID,数据库主键,全局唯一 */
private final Integer id;

/** 用户名 */
private final String name;

public User(Integer id, String name) {
this.id = id;
this.name = name;
}

public Integer getId() {
return id;
}

public String getName() {
return name;
}

// 你可以发现,在上面三个函数中包含大量的重复代码。
// 请尝试通过Predicate接口将上述代码抽取成一个公用的过滤器函数
// 并简化上面三个函数
public static List<User> filter(List<User> users, Predicate<User> predicate) {
List<User> results = new ArrayList<>();
for (User user: users) {
if(predicate.test(user)){
results.add(user);
}
}
return results;
}

public static void main(String[] args) {
List<User> res = filterUsersWithEvenId(Arrays.asList(new User(1,"a"),new User(2,"b")));
System.out.println(res);

List<User> res2 = filterZhangUsers(Arrays.asList(new User(1,"张三"),new User(2,"李四")));
System.out.println(res2);
}

// 过滤ID为偶数的用户
public static List<User> filterUsersWithEvenId(List<User> users) {
List<User> results = new ArrayList<>();
return filter(users, new Predicate<User>() {
@Override
public boolean test(User user) {
return user.id % 2 == 0 ;
}
});
}

// 过滤姓张的用户 lambda 表达式
public static List<User> filterZhangUsers(List<User> users) {
return filter(users, user -> user.name.startsWith("张"));
}

// 过滤姓王的用户
public static List<User> filterWangUsers(List<User> users) {
return filter(users, user -> user.name.startsWith("王"));
}
}

内部类详解

内部类

  • 用途:实现更加精细的封装
  • 可以访问外围类的实例方法
  • 非静态内部类
    • 和一个外围类实例相绑定
    • 可以访问外围类实例的方法
  • 静态内部类
    • 不和外围类实例绑定
    • 不可以访问外围实例的方法
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
public class Home {
void log(){
System.out.println(111);
}

public static void main(String[] args) {
new B(new Home()).xx();
}
static class B{

Home home;
// 通过构造器把外围类的实例注入到内部类来,使得静态内部类可以调用外围类的实例方法
B(Home home){
this.home = home;
}
void xx(){
home.log();
}
}

class C{
//编译器偷偷帮你注入一个 外围类的实例/对象
// private Home this$0;
// 这就是为什么非静态的内部类可以访问外部类的秘密所在
{
log();
}
}
}

静态内部类和非静态内部类使用的原则

  • 永远使用静态内部类,否则编译报错
    • 因为使用非静态内部类,编译器偷偷帮你注入一个 this$0的外围类实例,这个对象会占用空间的,如果不用,就浪费类空间

匿名内部类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Home {
public static void main(String[] args) {
// 匿名内部类
new Predicate<Object>(){
@Override
public boolean test(Object o) {
return true;
}
};
}
}

// 匿名相当于
class XXXX implements Predicate<Object>{

@Override
public boolean test(Object o) {
return false;
}
}
  • 好处就是非常的短小,逻辑紧密结合
  • 可以访问外围类的成员
  • 还可以变成 lambda 表达式

匿名类最后会变成什么

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 Home {
public static void main(String[] args) {
// 匿名内部类
new Predicate<Object>(){
@Override
public boolean test(Object o) {
return true;
}
};

new Predicate<Object>(){
@Override
public boolean test(Object o) {
return true;
}
};

new Predicate<Object>(){
@Override
public boolean test(Object o) {
return true;
}
};
}
}

// 此时三个匿名内部类就会变成

Home$1.class
Home$2.class
Home$3.class

ZB-014-java接口和抽象类

抽象类

  • 不可实例化
    • 反证法:假如我们允许实例化抽象类的实例,那么该实例调用抽象方法是怎样的?没有方法体?
  • 可以实例化的东西一定要补全所有的方法体
  • 可以包含抽象方法
  • 可以包含成员变量
1
2
3
4
5
6
7
8
9
public abstract class Animal{
public abstract void 跑();
}

public class Bird extends Animal{
public void 跑(){
// 跑的实现逻辑
}
}

接口

  • 接口部分的实现了多继承
  • 接口不是类
  • 接口的扩展
  • 接口只代表一种功能
  • 一个类只能继承一个类,但是却能实现若干的接口
1
2
3
4
5
6
7
8
9
10
11
12
13
interface Aaa {
// 接口里的成员默认是 public static final

/*
int a = 1;
等价于
public static final int a = 1;
而我们知道 final的应该大写
*/

// 所以正确的写法应该是
int A = 1;
}

接口可以包含什么

  • 若干个方法(默认 public)
  • 若干个常量(默认 public static final)
  • extends 接口
  • 默认方法
    • Since Java8
    • 一种妥协的产物
    • 可以用来实现 minxin
    • 菱形继承

在经典的接口里,java8之前,接口里的方法不能有方法体的 java8之后可以了

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
在 C++ 中 允许多继承
class A{
f(){ ....}
}
class B{
f(){ ....}
}

class C extends A,B{
//
}
// 菱形继承
new C().f(); // 调用的是谁?此时产生了歧义


// 而 java 是单根继承(不够灵活),于是提出了 “接口”允许多实现。
// 把接口的功能理解为一种能力
// 那如何区分 类继承体系和接口呢?
// 假设你是个老板Boss 你想找几个人干活,对于class来说就是找几个“人”来干活
// 对于interface来说就是 找几个“能干活”的人来
interface A{
f();
}
interface B{
f();
}
class C implements A,B{
f(){
....
}
}

// 而在java8 之后,这种现象被改变了。妥协了
// 当一个接口发布出去 就不能在改了,不能增添或减少成员,因为一旦成型,再次添加方法后,导致所有实现类都报错。必须实现接口的所有方法。

// 由于这种向后兼容性。所以这种发布之后,再也不修改的情况是个天真的想法——人的认知是有限的
// 所以在 java 工作体系运行20年之后,突然发现某些接口 如 List 想给当年的List添加一个 sort(); 但是很不幸 你不能打破“向后兼容性”, 这样导致 List的实现类全报错了,必须实现 sort方法才行,问题升级,你老板非要实现,但是你要改 N 多地方。

// 于是产生了一个妥协的产物——默认方法
这个 sort 就在 List 中
// 因为无法变成抽象方法,所以必须写实现,所以 sort 有一个默认的实现使得之前没有实现 sort方法的实现类也能正常工作

default void sort(Comparator<? super E> c){
Object[] a = this.toArray();
Array.sort(a,(Comparator) c);
ListIterator<E> i = this.listIterator();
for(Object e : a){
i.next();
i.set((E) e);
}
}

默认方法

  • java8 之前

    1
    2
    3
    4
    interface A{
    void a();
    void b();
    }
  • java8 之后

    1
    2
    3
    4
    5
    6
    7
    8
    interface A{
    void a();
    void b();
    default void c(){
    // 实现
    }
    }
    // 这样原来实现 A 接口的实现类就可以得到兼容

java8 接口默认方法引出的问题

  • 当年极力避免的 C++ 的多继承,导致父类有相同f();的问题出现了
  • 当年极力避免的 “二义性” 现在又妥协了
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    interface A{
    void a();
    default void c(){
    // 实现
    }
    }

    interface B{
    void a();
    default void c(){
    // 实现
    }
    }

    class C implatements A,B{
    {
    c(); // 此时报错了
    }
    }

接口和抽象类总结

共同点

  • 抽象的,不可实例化
  • 可包含抽象方法(没有方法体,非 static/private/final )

不同点

  • 抽象类是类可以包含类的一切,接口只能包含受限的成员(public static final)和方法(public abstract,java8之后可以 default的)
  • 抽象类只能单一继承,接口可以多实现

instanceof 不仅能检查是不是一个类的实例还可以检测是不是一个接口实现类的实例

什么是 API(application program interface)

什么是UI(User interface)

多态实战

Files.walkFileTree 当你用一个不知道意思的方法的时候,你最好看一下JDK文档 JDK的文档是最好的教材

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* Walks a file tree.
*
* <p> This method walks a file tree rooted at a given starting file. The
* file tree traversal is <em>depth-first</em> with the given {@link
* FileVisitor} invoked for each file encountered. 看到这就够了

public static Path walkFileTree(Path start,
Set<FileVisitOption> options,
int maxDepth,
FileVisitor<? super Path> visitor)
throws IOException
{ ... }
- 沿着给定的一个 目录
- 深度优先

FileVisitor 是什么?就是一个接口,如果你实现它,就可以在访问文件的过程中进行自定义控制

FileVisitor接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public interface FileVisitor<T> {

// 文件夹访问之前
FileVisitResult preVisitDirectory(T dir, BasicFileAttributes attrs)
throws IOException;

// 访问文件夹做什么
FileVisitResult visitFile(T file, BasicFileAttributes attrs)
throws IOException;

// 访问失败做什么
FileVisitResult visitFileFailed(T file, IOException exc)
throws IOException;

// 访问文件夹之后
FileVisitResult postVisitDirectory(T dir, IOException exc)
throws IOException;
}

FileVisitResult 访问文件的4个结果

1
2
3
4
5
6
public enum FileVisitResult {
CONTINUE,
TERMINATE,// 终止
SKIP_SUBTREE, // 忽略子树
SKIP_SIBLINGS; // 忽略所有兄弟
}

MyFileVisitor

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
package com.github.hcsp.polymorphism;

import java.io.IOException;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.List;

public class FileFilter {
public static void main(String[] args) throws IOException {
Path projectDir = Paths.get(System.getProperty("user.dir"));
Path testRootDir = projectDir.resolve("test-root");
if (!testRootDir.toFile().isDirectory()) {
throw new IllegalStateException(testRootDir.toAbsolutePath().toString() + "不存在!");
}

List<String> filteredFileNames = filter(testRootDir, ".csv");
System.out.println(filteredFileNames);
}

/**
* 实现一个按照扩展名过滤文件的功能
*
* @param rootDirectory 要过滤的文件夹
* @param extension 要过滤的文件扩展名,例如 .txt
* @return 所有该文件夹(及其后代子文件夹中)匹配指定扩展名的文件的名字
*/
public static List<String> filter(Path rootDirectory, String extension) throws IOException {
Files.walkFileTree(rootDirectory,new MyFileVisitor());
return null;
}
}

// 分割线,另一个 java文件里
// MyFileVisitor.java
public class MyFileVisitor implements FileVisitor {
@Override
public FileVisitResult preVisitDirectory(Object dir, BasicFileAttributes attrs) throws IOException {
System.out.println(dir);
return FileVisitResult.CONTINUE;
}

@Override
public FileVisitResult visitFile(Object file, BasicFileAttributes attrs) throws IOException {
System.out.println(file);
return FileVisitResult.CONTINUE;
}

@Override
public FileVisitResult visitFileFailed(Object file, IOException exc) throws IOException {
System.out.println(file);
return FileVisitResult.CONTINUE;
}

@Override
public FileVisitResult postVisitDirectory(Object dir, IOException exc) throws IOException {
return FileVisitResult.CONTINUE;
}
}

除此之外,我们可以找到 FileVisitor 的实现类(骨架类),因为我们不需要实现所有的接口

  • 找到了
1
2
3
4
5
6
7
public class FileFilterVisitor extends SimpleFileVisitor<Path>{
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
System.out.println(file);
return FileVisitResult.CONTINUE;
}
}

需求添加,我们需要知道过滤文件的扩展名,和返回过滤后的集合

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
package com.github.hcsp.polymorphism;

import java.io.IOException;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.List;

public class FileFilter {
public static void main(String[] args) throws IOException {
Path projectDir = Paths.get(System.getProperty("user.dir"));
Path testRootDir = projectDir.resolve("test-root");
if (!testRootDir.toFile().isDirectory()) {
throw new IllegalStateException(testRootDir.toAbsolutePath().toString() + "不存在!");
}

List<String> filteredFileNames = filter(testRootDir, ".csv");
System.out.println(filteredFileNames);
}

public static List<String> filter(Path rootDirectory, String extension) throws IOException {
FileFilterVisitor visitor = new FileFilterVisitor(extension);
Files.walkFileTree(rootDirectory,visitor);
return visitor.getFilterNames();
}
}

// 分割线,另一个 java文件里
// FileFilterVisitor.java
// 使用骨架实现
public class FileFilterVisitor extends SimpleFileVisitor<Path>{
private String extension;
private List<String> filterNames = new ArrayList<>();

public FileFilterVisitor(String extension) {
this.extension = extension;
}

public List<String> getFilterNames() {
return filterNames;
}

@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
System.out.println(file);
if(file.getFileName().toString().endsWith(extension)){
filterNames.add(file.getFileName().toString());
}
return FileVisitResult.CONTINUE;
}
}

再次升级需求:问题如下

  • 你的 FileFilterVisitor 不在一个文件里,看代码要分屏看两个文件,而且还需要定制构造器 传递extension参数

于是你把 FileFilterVisitor.java 移到 FileFilter.java

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
package com.github.hcsp.polymorphism;

import java.io.IOException;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.List;

public class FileFilter {
public static void main(String[] args) throws IOException {
Path projectDir = Paths.get(System.getProperty("user.dir"));
Path testRootDir = projectDir.resolve("test-root");
if (!testRootDir.toFile().isDirectory()) {
throw new IllegalStateException(testRootDir.toAbsolutePath().toString() + "不存在!");
}

List<String> filteredFileNames = filter(testRootDir, ".csv");
System.out.println(filteredFileNames);
}

public static List<String> filter(Path rootDirectory, String extension) throws IOException {
FileFilterVisitor visitor = new FileFilterVisitor(extension);
Files.walkFileTree(rootDirectory,visitor);
return visitor.getFilterNames();
}
}

class FileFilterVisitor extends SimpleFileVisitor<Path>{
private String extension;
private List<String> filterNames = new ArrayList<>();

public FileFilterVisitor(String extension) {
this.extension = extension;
}

public List<String> getFilterNames() {
return filterNames;
}

@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
System.out.println(file);
if(file.getFileName().toString().endsWith(extension)){
filterNames.add(file.getFileName().toString());
}
return FileVisitResult.CONTINUE;
}
}

即使移到了一个java文件里,但是你还是觉得麻烦

内部类(一个类包含在另一类中)

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
public class FileFilter {
public static void main(String[] args) throws IOException {
Path projectDir = Paths.get(System.getProperty("user.dir"));
Path testRootDir = projectDir.resolve("test-root");
if (!testRootDir.toFile().isDirectory()) {
throw new IllegalStateException(testRootDir.toAbsolutePath().toString() + "不存在!");
}

List<String> filteredFileNames = filter(testRootDir, ".csv");
System.out.println(filteredFileNames);
}

public static List<String> filter(Path rootDirectory, String extension) throws IOException {
FileFilterVisitor visitor = new FileFilterVisitor(extension);
Files.walkFileTree(rootDirectory,visitor);
return visitor.getFilterNames();
}

// 内部类
static class FileFilterVisitor extends SimpleFileVisitor<Path>{
private String extension;
private List<String> filterNames = new ArrayList<>();

public FileFilterVisitor(String extension) {
this.extension = extension;
}

public List<String> getFilterNames() {
return filterNames;
}

@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
System.out.println(file);
if(file.getFileName().toString().endsWith(extension)){
filterNames.add(file.getFileName().toString());
}
return FileVisitResult.CONTINUE;
}
}
}

还是觉得麻烦,因为要传递extension

匿名内部类

好处

  • 相近的两块逻辑组合到一起,避免两个逻辑在两个文件中,所带来的注意力不集中问题
  • 参数 extension 不需要构造器来传递了。可以直接在匿名内部类中访问
    • 匿名内部类可以毫无障碍的访问 外围的变量
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

public class FileFilter {
public static void main(String[] args) throws IOException {
Path projectDir = Paths.get(System.getProperty("user.dir"));
Path testRootDir = projectDir.resolve("test-root");
if (!testRootDir.toFile().isDirectory()) {
throw new IllegalStateException(testRootDir.toAbsolutePath().toString() + "不存在!");
}

List<String> filteredFileNames = filter(testRootDir, ".csv");
System.out.println(filteredFileNames);
}

// 匿名内部类
public static List<String> filter(Path rootDirectory, String extension) throws IOException {
List<String> names = new ArrayList<>();
Files.walkFileTree(rootDirectory,new SimpleFileVisitor<Path>(){
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
System.out.println(file);
if(file.getFileName().toString().endsWith(extension)){
names.add(file.getFileName().toString());
}
return FileVisitResult.CONTINUE;
}
});
return names;
}
}

以上就是多态的应用

  • 通过修改一小块的功能去完成一个更加灵活性的功能
  • 通过去覆盖(重写)一个方法来实现更加灵活的功能

  • 代码参考

  • 我的pr