0%

性能优化

为什么使用setState?

  • 修改了state之后,希望React根据最新的State来重新渲染界面,但是这种方式的修改React并不知道数据发生了变化

  • React并没有实现类似于Vue3中的Proxy的方式来监听数据的变化

  • 必须通过setState来告知React数据已经发生了变化

setState原理是使用一个Object.assign(this.state, newState)进行合并.

setState可以传入一个回调函数

  • 好处1:可以在回调函数中编写新的state的逻辑

  • 好处2:当前回调函数会将之前的state和props传递进来

1
2
3
4
5
6
7
8
9
10
changeText() {
this.setState((state, props) => {
console.log(this.state.message, this.props);
return {
message:"hello,oo"
}
})
}


setState异步更新

1
2
3
4
5
6
7
8
9
10
changeText() {
this.setState({message:"你好哦哦"})//并不会立刻更新message,
console.log("----",this.state.message);//此时还是constructor中的原message

//希望数据更新后,获取对应的结果执行一些逻辑代码
//那么可以设置setState中传入第二个参数,callback
this.setState({message:"hello"}, () => {
console.log("++",this.state.message);
})
}

为什么setState是异步的

  • 每次调用setState都进行一次更新,那么意味着render函数会被频繁调用,界面重新渲染
  • 最好的办法是获取到多个更新,知乎进行批量更新

如果同步更新了state但是还没有执行render函数,那么state和props不能保持同步

React18之前在setTimeout中setState操作是同步操作.,18之后就变成了批处理

React diff算法

复杂度O(n)

  • 同层节点之间相互比较,不会跨节点比较
  • 不同类型的节点,产生不同的树结构
  • 开发中,可以通过key来指定那些节点在不同的渲染下保持稳定

keys的作用 针对于列表元素

  • 在最后位置插入数据,此时有无key没有差别
  • 在前面插入数据,在没有key的情况下,所有的li都需要进行修改

当子元素拥有key时,react使用Key来匹配原有树上的子元素以及最新树上的子元素

使用注意:

  • key应该是唯一的
  • key不要使用随机数
  • 使用index作为key,对性能是没有优化的

render优化

父子组件嵌套时,修改一个子组件,其他很多组件没有必要重新render,他们调用render应该有个前提就是依赖的数据(state,props)发生改变时,再调用自己的render方法.

  • 控制render方法调用:shouldComponentUpdate
    • 该方法有两个参数; nextProps:修改之后 最新的props属性; nextState修改之后最新的state属性
1
2
3
4
5
6
shouldComponentUpdate(nextProps, newState) {
if(this.state.message !== newState.message) {
return true
}
return false
}

想省略shouldComponentUpdate,

  • 类组件继承PureComponent而不是component
  • 函数式组织采用memo将函数包裹起来

State数据不可变性

当this.state中属性是列表的时候注意:

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 class App extends Component {
constructor() {
super()
this.state = {
books: [
{ name: "css", price:121, count:1},
{ name: "js", price:52, count:1},
{ name: "html", price:11, count:1},
{ name: "c++", price:99, count:1},
]
}
}

addNewMessag(index) {
const newBook = {name:"xxx", price:25, count:1}
//1.直接修改原有state,重新设置一遍,这对于继承自purecomponent的组件,无法引起重新渲染render
this.state.books.push(newBook)
this.setState({books:this.state.books})

//正确做法,复制一份原数据修改
const books = [...this.state.books]
books.push(newBook)
books[index].count++
this.setState({books:books})
}

Ref获取DOM和组件

ref属性就是与获取关联.

DOM组件

通常不需要直接操作DOM原生,但部分情况可能需要

  • 管理焦点,文本旋转或媒体播放
  • 触发强制动画
  • 集成第三方DOM库
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
export class App extends PureComponent {
constructor(){
super()
this.state = {

}
this.titleRef = createRef()
}
getNativeDOM() {
//方式1:在React元素绑定一个ref字符串
console.log(this.refs.why);
//方式2:提前创建好ref对象,creatRef(),将创建出的对象绑定到元素
console.log(this.titleRef.current);
//方式3:
console.log(this.titleEl);
}

render() {
return (
<div>
<h2 ref="why">xixi</h2>
<h2 ref={this.titleRef}>xixi</h2>
<h2 ref={el => { this.titleEl = el}}>ohuoohuo</h2>
<button onClick={e => this.getNativeDOM()}>获取DOM</button>
</div>
)
}
}

获取组件

类组件

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
class HelloWorld extends PureComponent {
test() {
console.log("子组件实例");
}
render() {
return <h1>hello world</h1>
}
}

export class App extends PureComponent {
constructor(){
super()
this.state = {
}

this.hwRef = createRef()
}

getComponent() {
console.log(this.hwRef.current);//获取实例对象
this.hwRef.current.test()
}

render() {
return (
<div>
<HelloWorld ref={this.hwRef} />
<button onClick={e => this.getComponent()}>获取组件实例</button>
</div>
)
}
}

函数组件 -forwardRef

比如要绑定函数组件返回的某一个元素

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
const HelloWorld = forwardRef(function(props, ref) {
return (
<div>
<h1 ref={ref}>111</h1>
<p>222</p>
</div>
)
})

export class App extends PureComponent {
constructor(){
super()
this.state = {
}

this.hwRef = createRef()
}
getComponent() {
console.log(this.hwRef.current);//获取实例对象
this.hwRef.current.test()
}

render() {
return (
<div>
<HelloWorld ref={this.hwRef} />
<button onClick={e => this.getComponent()}>获取组件实例</button>
</div>
)
}
}

受控组件

对于input, textare, select,等表单组件加上value(value与state中属性绑定了)后就变成受控组件,此时是无法向里面输入内容,必须加上onChange

  • 文本框
  • 单选框
  • 多选框
  • selected框

单选框,多选框,注意此时不是从value中拿值而是从checked中取

多选框注意先需要创建一个数组.记录各个元素情况

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
import React, { PureComponent } from 'react'

export class App extends PureComponent {
constructor() {
super()

this.state = {
username:'',
password:'',
isAgree:true,
hobbies:[
{value:"sing", text:"唱", isChecked:false},
{value:"dance", text:"跳", isChecked:false},
{value:"rap", text:"rap", isChecked:false},
],
fruit:["orange"]
}
}

handleSubmitClick(event) {
//1.阻止默认行为,避免表单向action对象发送请求引起页面刷新
event.preventDefault()
//获取到所有的表单数据,对数据进行组织
console.log(this.state.username);
console.log(this.state.password);
console.log(this.state.hobbies.filter(item => item.isChecked).map(item => item.value));
console.log(this.fruit);
//以网络请求的方式,将数据传递给服务器(ajax)
}

handleInputChange(event) {
const keyName = event.target.name
this.setState({
[keyName]: event.target.value
})
}

handleAgreeChange(event) {
this.setState({isAgree:event.target.checked})
}

handleHobbiesChange(event, index){
const hobbies = [...this.state.hobbies]
hobbies[index].isChecked = event.target.checked
this.setState({ hobbies })
}

handleFruitChange(event) {
const options = Array.from(event.target.selectedOptions)
const values = options.map(item => item.value)
this.setState({ fruit: values })
}


render() {
const { username,password,isAgree, hobbies, fruit} = this.state

return (
<div>
<form onSubmit={e => this.handleSubmitClick(e)}>
//文本框
<label htmlFor="username">
用户: <input id='username' type='text' value={username} name='username' onChange={e => this.handleInputChange(e)}/>
</label>
<label htmlFor="password">
密码: <input id='password' type='password' value={password} name='password' onChange={e => this.handleInputChange(e)}/>
</label>

//单选框
<label htmlFor="agree">
<input id="agree" type="checkbox" checked={isAgree} onChange={e => this.handleAgreeChange(e)}/>同意协议
</label>

//多选框
<div>
您的爱好
{
hobbies.map((item,index) => {
return (
<label htmlFor={item.value} key={item.value}>
<input type="checkbox" id={item.value} checked={item.isChecked} onChange={e => this.handleHobbiesChange(e, index)}/>
<span>{item.text}</span>
</label>
)
})
}
</div>
//勾选框
<select value={ fruit } onChange={e => this.handleFruitChange(e)} multiple>
<option value="apple">apple</option>
<option value="banana">banana</option>
<option value="orange">orange</option>
</select>

<button type='submit'>提交</button>
</form>
</div>
)
}
}

export default App

脚手架Scaffold

快速生成项目的工程化结构.每个项目的基本工程化结构是相似的.

PWA(Progressive Web App)

  • PWA是一个网页,可以通过Web技术编写出一个网页应用
  • 随后添加上App Manifest和Service Worker来实现PWA的安装和离线功能

PWA的作用

  • 可以添加至主屏幕,点击主屏幕图标实现动画以及隐藏地址栏

  • 实现离线缓存功能,即使用户手机没网络,依然可以使用一些离线功能

  • 实现消息推送

组件化

划分方式

不同方式可以分成更多类的组件

组件的定义方式:

  • 类组件
  • 函数组件

组件内部是否有状态要维护:

  • 无状态stateless Component
  • 有状态stateful Component

不同的职责

  • 展示型组件Presentational Component
  • 容器型组件(Container Component)

函数组件,无状态组件,展示型组件主要关注UI的展示

类组件,有状态组件,容器型组件主要关注数据逻辑

类组件

  • 名称大写字母开头

  • 需要继承自React.Component

  • 类组件必须实现render函数

render():被调用时,会检查this.props和this.state的变化并返回以下类型之一:

React元素:

  • 通常由JSX创建

  • 会被React渲染为DOM节点, 会被渲染成自定义组件.它们都是React元素

数组/fragments:使得可以返回多个元素

Portals:可以渲染子节点到不同的DOM子树中

字符串或数值类型:在DOM中会被渲染为文本节点

布尔类型或null:什么都不渲染

函数式组件(无hook情况下)

返回值充当render()的角色

  • 无生命周期
  • this关键字不能指向组件实例(因为没有组件实例)
  • 没有内部状态

生命周期

创建到销毁的过程

  • 装载Mount:组件第一次在DOM树中被渲染的过程
  • 更新过程Update:组件状态发生变化,重新更新渲染的过程
  • 卸载过程Unmount:组件从DOM树中被移除的过程

生命周期函数

生命周期函数: React内部为了告诉我们当前处于哪个阶段,会对我们那组件内部实现的某些函数进行回调

componentDidMount:组件挂载到DOM上时,就会回调

  • 依赖DOM的操作就可以在此处进行
  • 发送网络请求最好的地方
  • 添加一些订阅(compponentWillUnmount取消订阅)

componentDidUpdate函数:组件发生更新时,就会回调

  • 组件更新后可以在此次对DOM进行操作
  • 对更新前后的propos进行了比较,也可以选择在此次进行网络请求(当props未发生变化时,则不会执行网络请求)

componentWillUnmount函数:组件即将被移除时,就会回调

  • 执行必要的清理操作
  • 清除timer,取消网络请求,清除在componentDidMount()中创建的订阅

不常用的生命周期函数

  • getSnapshowBeforeUpdate:在React更新DOM之前回调的一个函数,可以获取DOM更新前的一些信息比如滚动位置
  • shouldComponentUpdate:性能优化

组件嵌套

App的组件是Header, Main, Footer组件的父组件

Main组件是Banner,PorductList的父组件

组件通信

  • App中使用了多个Header,每个地方Header展示的内容不同,那么我们就需要使用者传递给Header一些数据,让其进行展示
  • 在Main中请求了Banner数据和ProductList数据,那么就需要传递给他们进行展示
  • 子组件中发生了事件,需要父组件拉力完成某些操作,那么就需要子组件向父组件传递事件

父组件通过 属性=值的形式来传递给子组件数据

子组件通过props参数来获取父组件传递过来的数据

父传子

1
2
3
4
5
6
7
8
9

render() {
const {banners} = this.state
return (
<div className='main'>
<MainBanner banners={banners} title="xixihaha"/>
<MainProductList/>
</div>
)

propTypes

对于传递给子组件的数据,有时候我们可能希望验证

类型验证和传入默认值

1
2
3
4
5
6
7
8
MainBanner.propTypes = {
banners: PropTypes.array.isRequired,
title:PropTypes.string
}
MainBanner.defaultProps = {
banners: [],
title:"默认标题"
}

子传父

通过props传递消息,让父组件给子组件传递一个回调函数,在子组件调用这个函数即可.

插槽slot实现

抽取一个组件,不能将组件中的内容限制为固定的div,span等等

让使用者决定某一块区域放什么内容.

实现方法:

  • 组件的children

    • 注意只有一个插槽时,children只有一个.

      因此弊端: 通过索引值获取传入的元素很容易出错,不能精准的获取传入的原生

  • props属性传递React元素 (常用)

  • 作用域插槽,函数回调

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
//1.组件的children
export class App extends Component {
render() {
return (
<div>
<NavBar>
<button>1</button>
<button>2</button>
<button>3</button>
</NavBar>

export class NavBar extends Component {
render() {
const { children } = this.props
return (
<div className='nav-bar'>
<div className="left">{children[0]}</div>
<div className="center">{children[1]}</div>
<div className="right">{children[2]}</div>
</div>
)
}
}


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
//2.props属性传递React元素


export class App extends Component {

//根据子组件中的内容决定父组件的tag
getTabItem(item){
if(item==="xixi"){
return<span>{item}</span>
}else{
return <i>{item}</i>
}

}
render() {
return (
<div>
<NavBar leftSlot={item => <button>{item}</button>} centerSlot= {item => <button>{item}</button>} rightSlot={item => this.getTabItem(item)} />
</div>
)
}
}


export class NavBar extends Component {
render() {
const {leftSlot,centerSlot,rightSlot} = this.props
return (
<div className='nav-bar'>
<div className="left">{leftSlot(数据)}</div>
<div className="center">{centerSlot(数据)}</div>
<div className="right">{rightSlot(数据)}</div>
</div>
)
}
}

Context应用场景

跨多组件进行共享(地区偏好,UI主题,用户登录状态,用户信息)\

createContext

方案一:

  1. 创建一个context 单独的文件,使用React.createContext
  2. 在父元素中,通过ThemeContext.Provider中的value属性为后代传递数据
  3. 在子组件导入context单独文件,并将该Context导入,即可以使用ThemeContext中Value的数据

遇到组件需要多个Context场景:需要使用Context.Consumer特性

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
//1.创建context
import React from "react"
const ThemeContext = React.createContext()

export default ThemeContext


//2.调用Provider
export class App extends Component {
constructor() {
super()
this.state = {
info:{name:"alug", age:30}
}
}
render() {
const { info } = this.state

return (
<div>
<UserContext.Provider value={{color:"red", size:"30"}}>
<ThemeContext.Provider value={{color:"red", size:"30"}}>
<Home {...info}/>
</ThemeContext.Provider>
</UserContext.Provider>
</div>
)
}
}

//3. context中取值
export class HomeInfo extends Component {
render() {
console.log(this.context);
return (
<div>
<h2>HomeInfo {this.context.color}</h2>

<UserContext.Consumer>
{
value => {
return <h2>Info User: {value.color}</h2>
}
}
</UserContext.Consumer>
</div>
)
}
}

HomeInfo.contextType = ThemeContext

事件总线的外部库 hy-event-store

创建一个event-store对象A, 子组件通过A.emit给父组件发送数据,父组件通过A.on监听.

通常监听设置componentDidMount中,然后在componentwillUnmount()中取消监听

内联方式

一切都写死了,不能动态修改某些样式

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 class App extends PureComponent {
constructor() {
super()

this.state = {
titleSize: 30
}
}

addTitleSize() {
this.setState({ titleSize: this.state.titleSize + 2})
}

render() {
const { titleSize } = this.state

return (
<div>
<button onClick={e => this.addTitleSize()}></button>
<h2 style={{color:"red", fontSize:`${titleSize}px`}}>xixi</h2>
<p style={{color:"red", fontSize:"30px"}}>I'm contenu</p>
</div>
)
}
}

普通CSS(不建议)

import ‘xxx.css’

问题在于普通CSS没有自己的作用域容易造成冲突,作用于全局.

CSS Modules

将CSS文件名改为’xxx.module.css’.可以实现局部作用域

文件中导入时需要自定义名导入使用,如import appStyle from “./App.module.css”

每个样式使用则按照{appStyle.title}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import appStyle from "./App.module.css"

export class App extends PureComponent {
render() {
return (
<div>
<h2 className={appStyle.title}>Title</h2>
<p className={appStyle.content}>This is content</p>
</div>
)
}
}

export default App

CSS in JS

由第三方库来支持:

  • styled-components
  • emotion
  • glamorous
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
import styled from "styled-components";
import * as vars from './variable'

//5可以从一个单独的文件中引入变量
// export const primaryColor = "#ff8800"
// export const secondColor = "#ff7788"
// export const fontSize = "25px"



//1.基本使用
export const AppWrapper = styled.div`
.footer{
border: 1px solid red;
}
`

//2.子元素单独抽取到一个样式组件
//3.接受外部传入的props
export const SectionWrapper = styled.div.attrs(props => { //4.通过attrs给标签模版字符串提供属性
return {
tColor: props.color || "blue"
}
})`
border: 1px solid red;

.title {
font-size: ${props => props.size};
color: ${props => props.tcolor};

&:hover {
background-color: black;
}
}

.content {
font-size: ${vars.fontSize};
color: ${vars.primaryColor};
}
`

//6.样式的继承
const HYbutton = styled.button`
padding: 8px 30px;
border-radius: 5px;
`

export const HYWarnButton = styled(HYbutton)`
background-color: red;
color:#fff;
`

动态添加类

classnames外部包

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
import React, { PureComponent } from 'react'
import classNames from 'classnames'


export default class App extends PureComponent {
constructor () {
this.state = {
isbbb:true,
isccc:true
}
}
render() {
const { isbbb, isccc } = this.state
const classList = ["aaa"]
if (isbbb) classList.push("bbb")
if (isccc) classList.push("ccc")
const classname = classList.join(" ")


return (
<div>
{/* 第一种方式:三元运算符直接写 */}
<h2 className={`aaa ${isbbb ? 'bbb': ''} ${isccc ? 'ccc':' '}`}>哈哈</h2>

{/* 第二种方式:写入一个列表 */}
<h2 className={classname}></h2>

{/* 第三种:使用classnames包 */}
<h2 className={classNames("aaa", {bbb:isbbb, ccc:isccc})}></h2>

</div>
)
}
}

高阶组件

Higher-order Components(HOC)

高阶组件是以参数为组件,返回值为新组件的函数

本质上对组件进行一次拦截,然后进行处理,返回一个处理后的新组件

缺陷:

需要再原组件上进行包裹或者嵌套,如果大量使用HOC,将会产生非常多的嵌套,导致调试困难

会劫持props,在不遵守约定的情况下会产生冲突

场景 1:props

可以对函数组件和类组件进行处理.

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
import React, { PureComponent } from 'react'


function enhancedUser(OriginComponent) {
class NewComponent extends PureComponent {
constructor(props) {
super(props)

this.state = {
userInfo : {
name: "codewhy",
level:99
}
}
}

render() {
return <OriginComponent {...this.props} {...this.state.userInfo}/>
//{...this.props}代表着app中传给子组件的数据. {...this.state.userInfo}代表高阶组件传入的数据
}
}
}

const Home = enhancedUser(function(props) {
return (<h1>Home: {props.name} - {props.level}- {props.banners}</h1>)
//经过加强后,此时父组件和高阶组件传入的数据都可以使用 都在props中
})


export class App extends PureComponent {
render() {
return (
<div>
<Home banners={["xixihaha", "oooaa"]}/>
<div>{this.props.name}</div> //类组件被包裹后,加强组件传入的数据同样也在其props中找
</div>
)
}
}

export default enhancedUser(App) //对一个类组件处理,只用在最后将其包裹

真实场景1: context共享

当存在多个组件需要使用context传递,此时传统方法需要再子组件中写

1
2
3
4
5
6
7
<UserContext.Consumer>
{
value => {
return <h2>Info User: {value.color}</h2>
}
}
</UserContext.Consumer>

从而获取父组件传递的参数,太繁琐.

因此可以对这个包裹部分写成一个高阶组件.使用高阶组件包装子组件,export default withTheme(sonComps) 则可以实现简化.

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
//父组件
import React, { PureComponent } from 'react'
import ThemeContext from './context/theme_context'
import Product from './Product'

export class App extends PureComponent {
render() {
return (
<div>
<ThemeContext.Provider value={{color:"red", size:30}}> //context传递的
<Product title={"xixihaha"} /> //props传递的
</ThemeContext.Provider>
</div>
)
}
}

export default App


//单独使用一个js文件,创建一个Context对象
import { createContext } from "react";
const ThemeContext = createContext()
export default ThemeContext


//高阶组件包装Context.consumer
import ThemeContext from "./theme_context"
function withTheme(OriginComponent) {
return (props) => {
return(
<ThemeContext.Consumer>
{
value => {
return <OriginComponent {...value} {...props}/>
//注意此处向组件中传递了context的value数据和父组件props的数据,因此子组件取数据时,直接在props统一取就行
}
}
</ThemeContext.Consumer>
)
}
}

export default withTheme


//子组件
import React, { PureComponent } from 'react'
import withTheme from './context/with_theme'

export class Product extends PureComponent {
render() {
const { color, size, title } = this.props //直接存props中取数据
return (
<div>Product:{color}- {size}-{title}</div>
)
}
}

export default withTheme(Product) //调用高阶函数,可以直接

真实场景2:检验授权

传统写法,写一个三元运算{ isLogin?:”需要登录”},为True展示页面,false不展示. 如果存在多个需要登录的地方,那么需要多次重复写这个三元运算.

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
//检验授权的高阶组件
function loginAuth(OriginComponent) {
return props => {
// ex 从LocalStorage获取token
const token = localStorage.getItem("token") //检验授权的方式
if (token) {
return <OriginComponent {...props}/>
} else {
return <h2> Please Login</h2>
}
}
}

export default loginAuth

//需要授权后才能展示的页面
import React, { PureComponent } from 'react'
import loginAuth from './login_auth'

export class cart extends PureComponent {
render() {
return (
<div>cart</div>
)
}
}

export default loginAuth(cart) //高阶组件包裹

//主App
import React, { PureComponent } from 'react'
export class App extends PureComponent {
render() {
return (
<div>
<cart/> //此时不需要任何额外处理
</div>
)
}
}

export default App

真实场景3:生命周期

测试一个页面的渲染时间

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
function logRenderTime(OriginComponent) {
return class extends PureComponent { //直接返回类时,可以省略类名
UNSAFE_componentWillMount() {
this.beginTime = new Date().getTime()
}

componentDidMount() {
this.endTime = new Date().getTime()
const interval = this.endTime - this.beginTime
console.log(`页面划分${interval}ms渲染`);
}

render() {
return <OriginComponent {...this.props}/>
}
}
}

export default logRenderTime

//某个页面
import React, { PureComponent } from 'react'
export class App extends PureComponent {
render() {
return (
<div>
<h1>xxx</h1>
</div>
)
}
}

export default logRenderTime(App)

Portals的使用

希望渲染的内容独立与父组件,甚至独立于当前挂载到的DOM元素(默认挂载到id为root的DOM元素上)

1
2
3
4
5
6
7
8
9
10
11
12
export class App extends PureComponent {
render() {
return (
<div>
<h1>App H1</h1>
{
createPortal(<h2>appp h2</h2>, document.querySelector("#why")) //(插入的内容, 挂载的地方)
}
</div>
)
}
}

Fragment的用法

可以用于包裹元素块,免得每次总要用div包起来.

和<>等价,不过注意,对于需要绑定key(如列表遍历的情况),只能用Fragment

1
2
3
4
5
6
7
8
9
export default class App extends PureComponent {
render() {
return (
<Fragment>
<h2>xixihaha</h2>
</Fragment>
)
}
}

严格模式

包裹相关组件

1
2
3
4
5
6
7
8
9
10
11
export default class App extends PureComponent {
render() {
return (
<StrictMode>
<div>
<h2>xixihaha</h2>
</div>
</StrictMode>
)
}
}

严格模式检测什么?

  • 识别不安全的生命周期

  • 使用过时的ref API

  • 检查意外的副作用

    • 这个组件的constructor会被调用两次

    • 这是严格模式下故意进行的操作,让你来查看在这里写的一些逻辑代码被调用多次是,是否会产生一些副作用

    • 在生产环境下,是不会被调用两次的

React过渡动画react-transition-group

四个主要组件

  • Transition:和平台无关的组件
  • CSSTransition:通常使用CSSTransition完成过渡动画效果
  • SwitchTransition:两个组件显示和隐藏切换时,使用该组件
  • TransitionGroup:将多个动画组件包裹在其中,一般用于列表中元素的动画

CSSTransition

执行过程中,有三个状态:appear,enter,exit

它有三种状态,需要定义对应的CSS样式

  • 第一类,开始状态,对应的类 -appear, -enter, exit
  • 第二类,执行动画,对应的类 -appear-active, -enter-active, -exit-active
  • 第三类,执行结束,对应的类是-appear-done, -enter-done, -exit-done

CSSTransition常见属性:

  1. className:动画class的名称
  2. timeout:过渡动画的时间
  3. appear是否在初次进行添加动画(需要和in同时设置为true)
  4. unmountOnExit:退出后卸载组件
  5. 部分构造函数,检测动画执行过程,
    1. onEnter:在进入动画之前出发
    2. onEntering:在应用进入动画时被触发
    3. onEntered:在应用进入动画结束后被触发
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
.why-enter {
opacity:0
}

.why-enter-active {
opacity: 1;
transition: opacity 2s ease;
}

.why-exit {
opacity: 1;
}

.why-exit-active {
opacity: 0;
transition: opacity 2s ease;
}

--------------------------------------------
export class App extends PureComponent {
constructor(props) {
super(props)
this.state = {
isShow:true
}
}


render() {
const { isShow} = this.state

return (
<div>
<button onClick={e => this.setState({isShow:!isShow})}>Change</button>
<CSSTransition in={isShow} unmountOnExit={true} classNames="why" timeout={2000} appear
onEnter={e=> console.log("开始进入")}> //属性in表示在true和false中切换
<h2>haha</h2>
</CSSTransition>
</div>
)
}
}

SwitchTransition

主要属性mode:

in-out:新组件先进入,旧组件再移除

out-in:旧组件先移除,新组件再进入

把CSSTransition包起来就可以.主要此时在CSSTransition用的是key而不是 in

1
2
3
4
5
6
7
8
9
10
return (
<div>
<button onClick={e => this.setState({isLogin: !isLogin)}>{ isLogin? "退出":登录}</button>
<SwitchTransition mode='out-in'>
<CSSTransition key={isLogin?"exit":"login"} classNames="login" timeout={2000}>
<h2>haha</h2>
</CSSTransition>
</SwitchTransition>
</div>
)

TransitionGroup

1
2
3
4
5
6
7
      <TransitionGroup component="ul">
<CSSTransition key={index} classNames="book" timeout={2000}>
<li>
<span>{item.name}-{item.price}</span>
</li>
</CSSTransition>
</TransitionGroup>

Redux

纯函数

  • 确定的输入,一定会有确定的输出
  • 函数执行过程中,不能有副作用

副作用:

执行一个函数时除了返回函数值之外,还对调用函数产生了附加的影响,比如修改了全局变量,修改参数或改变外部的存储

Store=>state

数据存储的地方

Action=>修改state

所有数据的变化必须通过dispatch action来更新

action是一个普通的js对象,用来描述这次更新的type和content

reducer=>纯函数

  • reducer是一个纯函数
  • 将传入的state和action结合起来生产一个新的state

代码优化

  1. 将派发的action生产过程放到一个actionCreators函数中
  2. 将定义的所有actionCreators函数,放到一个独立的文件中: actionCreators.js
  3. actionCreators和reducer函数中使用字符串常量是一致的,所有将常量抽取到一个独立的constants的文件中
  4. 将reducer和默认值(initalState)放到一个独立的reducer.js文件中,而不是在index.js中
  5. index.js用于创建store和导出store
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
//1. 2. action
const { ADD_NUMBER, CHANGE_NAME } = require("./constant")

// actionCreators: 帮助创建action
const changeNameAction = (name) => ({
type: CHANGE_NAME,
name
})

const addNumberAction = (num) => ({
type: ADD_NUMBER,
num
})

module.exports = {
changeNameAction,
addNumberAction
}
--------------------------------------------------------------------------------

//3. constants
const ADD_NUMBER = "add_number"
const CHANGE_NAME = "change_name"

module.exports = {ADD_NUMBER, CHANGE_NAME}



//4.将reducer和默认值(initalState)放到一个独立的reducer.js文件中
const { ADD_NUMBER, CHANGE_NAME } = require("./constant")

//初始化的数据
const initialState = {
name: "why",
num:100
}

// 定义reducer函数:纯函数
//两个参数
//参数一:store中目前保存的state
//参数二:本次需要更新的action(dispatch传入的action)
function reducer(state = initialState, action) {
console.log(state, action);
// 有新数据更新,那么返回新的state
// 没有新数据,那么返回之前的state
switch(action.type) {
case CHANGE_NAME:
return { ...state, name: action.name}
case ADD_NUMBER:
return {...state, counter: state.counter + action.num}
default:
return state
}
}

module.exports = {reducer}



//util

const store = require("./store")
const { addNumberAction, changeNameAction} = require("./store/actionCreators")


const unsubscribe = store.subscribe(() => {
console.log("订阅数据的变化",store.getState())
})

store.dispatch(changeNameAction("curry"))

Redux的三大原则

单一数据源

  • 整个应用程序的state被存储在一颗Object tree中, 并且这个object tree只存储在一个store中
  • Redux并没有强制让我们不能创建多个Store,但是那样做并不利于数据的维护
  • 单一的数据源可以让整应用程序的state变得方便维护,追踪,修改

State是只读的

  • 唯一修改State的方法一定是触发action,不要试图在其他地方通过任何方式来修改State
  • 这样确保了View或网络请求都不能直接修改state,它们只能通过action来描述自己想要如何修改state
  • 这样可以保证所有的修改都被集中化处理,并且按照严格的顺序来执行,所有不需要担心race condition的问题

使用纯函数来执行修改

  • 通过reducer将旧state和actions联系在一起,并且返回一个新的State
  • 随着应用程序的复杂度增加,我们可以将reducer拆分成多个小的reducers,分别操作不同的state tree的一部分
  • 但是所有的reducer都应该是纯函数,不能茶是你任何副作用

StreamAPI

  • Stream API关注的是多个数据的计算(排序、查找、过滤、映射、遍历等,面向CPU。集合关注的数据的存储,向下内存的。
  • Stream API之于集合,类似于SQL之于数据表的查询。

使用说明

  • Stream不会存储元素
  • Stream不会改变源对象。相反,他们会返回一个持有结果的新Stream
  • Stream是延迟执行的。这意味着他们会等到需要结果的时候才执行。即一旦执行终止操作,就执行中间操作链,并产生结果
  • Stream一旦执行了终止操作,就不能再调用其它中间操作或终止操作

Stream执行流程

  1. Stream的实例化
  2. 一系列的中间操作
  3. 执行终止操作

实例化:

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
//创建Stream方式一:通过集合
@Test
public void test1(){
List<Employee> list = EmployeeData.getEmployees();
// default Stream<E> stream():返回一个顺序流
Stream<Employee> stream = list.stream();
// default Stream<E> parallelStream():返回一个并行流
Stream<Employee> stream1= list.parallelStream();
}

//创建Stream方式二:通过数组
@Test
public void test2(){
//通过调用Arrays的static<T> Stream<T> stream(T[] array):返回一个流
Integer[] arr = new Integer[]{1,2,3,4,5};
Stream<Integer> stream = Arrays.stream(arr);

int[] arr1 = new int[]{1,2,3,4,5};
IntStream stream1 = Arrays.stream(arr1);

}

//创建Stream方式三:通过Stream的of()
@Test
public void test3(){
Stream<String> stream = Stream.of("Aa", "bb", "cc");
}

中间操作

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
//1.筛选与切片
@Test
public void test1(){
//filter(Predicate p)--接受Lambda,从流中排除某些元素
//ex.查询员工表中薪资大于7000的员工信息
List<Employee> list = EmployeeData.getEmployees();
Stream<Employee> stream = list.stream();
stream.filter(emp -> emp.getSalary()>7000).forEach(System.out::println);

//limit(n) --截断流,使其元素不超过给定数量
list.stream().limit(4).forEach(System.out::println);
//skip(n) -- 跳过元素。返回一个扔掉前n个元素的流
list.stream().skip(5).forEach(System.out::println);
//distinct() --筛选,通过流所生成元素的hashCode()和equals()去除重复元素
list.stream().distinct().forEach(System.out::println);
}

//2.映射
@Test
public void test2(){
//map(Function f)接受一个函数作为参数,将元素转化成其他形式或提取信息,该函数会应用到每个元素
List<String> list = Arrays.aslist("aa","bb","cc","dd");
list.stream().map(str -> str.toUpperCase()).forEach(System.out::println);

//Ex:获取员工姓名大于3的员工
List<Employee> employees = EmployeeData.getEmployees();
employees.stream().filter(emp->emp.getName().length()>3).forEach(System.out::println);

//Ex.获取员工姓名长度大于3的员工的姓名
List<Employee> employees = EmployeeData.getEmployees();
employees.stream().filter(emp->emp.getName().length()>3).map(emp->emp.getName()).forEach(System.out::println);
}
//3.排序
@Test
public void test3(){
//sorted()- 自然排序
Integer[] arr = new Integer[]{12,3,4,5,6,23,45,1};
String[] arr1 = new String[]{"GG","DD","FF","SS","JJ"};
Arrays.stream(arr).sorted().forEach(System.out::println);

//sorted(Comparator com) - 定制排序
List<Employee> list = EmployeeData.getEmployees();
list.stream().sorted((e1,e2)-> e1.getAge() - e2.getAge()).forEach(System.out::println);

}

终止操作

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 StreamAPITest3 {
//1-匹配与查找
@Test
public void test1(){
List<Employee> list = EmployeeData.getEmployees();

//allMatch(Predicate p) -检测是否匹配所有元素
System.out.println(list.stream().allMatch(emp->emp.getAge()>18));
//anyMatch(Predicate p)-检查是否至少匹配一个元素
System.out.println(list.stream().anyMatch(emp->emp.getAge()>18));
//findFirst-返回第一个元素
System.out.println(list.stream().findFirst(emp->emp.getAge()>18));
//Count-返回流中元素的总个数
System.out.println(list.stream().filter(emp->emp.getSalary()>7000).count());
//max(Comparator c)-返回流中最大值
list.stream().max((e1,e2)-> Double.compare(e1.getSalary(), e2.getSalary()));
//min(Comparator c)-返回流中最大值
//forEach(Consumer c)-内部迭代
list.stream().forEach(System.out::println);
}

//2-归约
@Test
public void test3(){
//reduce(T identity, BinaryOperator)-可以将流中元素反复结合起来,得到一个值
List<Integer> list = Arrays.asList(1,2,3,4,5,6,7);
list.stream().reduce(0,(x1,x2)->x1+x2);

//reduce(BinaryOperator)
//ex.计算公司所有员工工资总和
list<Employee> employeeList = EmployeeData.getEmployees();
System.out.println(employeeList.stream().map(employee->employee.geSalary()).reduce((salary1,salary2)->Double.sum(salary1,salary2)));
System.out.println(employeeList.stream().map(employee->employee.geSalary()).reduce(Double::sum);

}
//3.收集
@Test
public void test4(){
//collect(Collector c)-将流转换位其他形式。接受一个Collector接口的实现,用于给Stream中元素做汇总方法
//ex1:查找工资大于6000的员工,返回一个List
List<Employee> list = EmployeeData.getEmployees();
List<Employee> list1 = list.stream().filter(emp-> emp.getSalary()>6000).collect(Collectors.toList());
list1.forEach(System.out::println);
//ex2:员工年龄排序
List<Employee> list2 = list.stream().sorted((e1,e2)->e1.getAge()-e2.getAge()).collect(Collectors.toList());
list2.forEach(System.out::println);
}
}

Lambda表达式

使用举例

1
Comparator<Integer> com2 =(o1, o2)-> Integer.compare(o1, o2);

格式举例

lambda形参列表 ->Lambda方法体

->:Lambda操作符或箭头操作符

->的左边:lambda形参列表,对应着要重写接口中的抽象方法的形参列表

->的右边:lambda体,对应着接口的实现类要重写的方法的方法体

无参,无返回值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Test
public void test(){
Runnable r1 = new Runnable() {
@Override
public void run() {
System.out.println("xixihaha");
}
};
r1.run();

//Lambda表达式写法
Runnable r2 = () ->{
System.out.println("xixihaha");
};
r2.run();
}

需要一个参数,但无返回值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
    Consumer<String> con = new Consumer<String>() {
@Override
public void accept(String s) {
System.out.println(s);
}
};
con.accept("嘻嘻哈哈");

//Lambda表达式写法
Consumer<String> con1 = (String s) ->{
System.out.println(s);
};
con1.accept("嘻嘻哈哈");
}

数据类型可以省略,因为可以推断除了,类型推断

1
2
3
4
5
6
7
8
9
10
11
12
@Test
public void test4(){
Consumer<String> con1 = (String s) ->{
System.out.println(s);
};
con1.accept("xixihaha");
//Lambda表达式写法
Consumer<String> con2 = (s) -> {
System.out.println(s);
};
con2.accept("xixihaha");
}

若只需要一个参数,参数小括号可以省略

1
2
3
4
5
6
7
8
9
10
11
12
@Test
public void test5(){
Consumer<String> con1 = (String s) ->{
System.out.println(s);
};
con1.accept("xixihaha");
//Lambda表达式写法
Consumer<String> con2 = s -> {
System.out.println(s);
};
con2.accept("xixihaha");
}

两个或以上的参数,多条执行语句,并且有返回值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Test
public void test6(){
Comparator<Integer> com1 = new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
System.out.println(o1);
System.out.println(o2);
return o1.compareTo(o2);
}
};
//Lambda表达式写法
Comparator<Integer> com2 = (o1,o2) -> {
System.out.println(o1);
System.out.println(o2);
return o1.compareTo(o2);
};
}

当Lambda体只有一条语句时,return与大括号若有,都可以忽略

1
2
3
4
5
6
7
8
9
10
11
12
13
@Test
public void test7(){
Comparator<Integer> com1 = new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
System.out.println(o1);
System.out.println(o2);
return o1.compareTo(o2);
}
};
//Lambda表达式写法
Comparator<Integer> com2 = (o1,o2) -> o1.compareTo(o2);
}

Lambda表达式本质

  • 一方面,lambda表达式作为接口的实现类的对象。
  • lambda表达式是一个匿名函数

函数式接口

定义:

  • 如果接口中只声明有一个抽象方法,则此接口就称为函数式接口。

  • 只有给函数式接口提供实现类的对象时,我们才可以使用lambda表达式

常见的函数式接口

消费型接口:Consumer void accept(T t)

供给型接口:Supplier T get()

函数型接口: Function<T,R> R apply(T t)

判断型接口:Predicate boolean test(T t)

Lambda表达式的语法规则总结

->的左边:lambda形参列表,参数的类型都可以省略。形参如果只有一个,则一对()也可以省略

->的右边:lambda体,对应着重写的方法的方法体。如果方法体只有一行执行语句,则一对{}省略

方法引用

举例

Integer :: compare

理解

  • 方法引用,可以看作是基于Lambda表达式的进一步刻画
  • 当需要提供一个函数式接口的实例时,我们可以使用lambda表达式提供此实例
    • 当满足一定条件的情况下,我们还可以使用方法引用或构造器替换lambda表达式

本质

方法引用作为函数式接口的实例

格式

类(或对象)::方法名

具体使用情况说明

Case1: 对象::实例方法

要求:函数式接口中的抽象方法a与其内部实现时调用的对象的某个方法b的形参列表和返回值都一致。此时可以考虑使用方法b实现对方法a的替换和覆盖。此替换或覆盖即为方法应用。

注意:此方法b是非静态的方法,需要对象调用

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
//情况1: 对象::实例方法
//Consumer中的void accept(T t)
//PrinStream中的void println(T t)
@Test
public void test1(){
//1.
Consumer<String> con1 = new Consumer<String>() {
@Override
public void accept(String s) {
System.out.println(s);
}
};
//2.
Consumer<String> con2 = s -> System.out.println(s);

//3.
Consumer<String> con3 = System.out::println;
con3.accept("hello");
}

//Supplier中的T get()
//Employee中的String getName()
@Test
public void test2(){
//1.
Employee emp = new Emplyoee("alugg",15);
Supplier<String> sup1 = new Supplier<String>() {
@Override
public String get() {
return emp.getName();
}
};
System.out.println(sup1.get());
//2.lambda表达式
Supplier<String> sup2 = ()->emp.getName();
System.out.println(sup2.get());
//3.方法引用
Supplier<String> sup3 = emp::getName;
}

Case2: 类::静态方法

要求:函数式接口中的抽象方法a与其内部实现时调用的类的某个静态方法b的形参列表和返回值都相同(一致)。此时可以考虑使用方法b实现对方法a的替换和覆盖。此替换或覆盖即为方法应用。

注意:此方法b是静态的方法,需要对象调用

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
//Comparator中的int compare(T t1, T t2)
//Integer中的int compare(T t1, T t2)
@Test
public void test3(){
//1.
Comparator<Integer> com1 = new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return Integer.compare(o1, o2);
}
};
//2.
Comparator<Integer> com2 = (o1,o2) -> Integer.compare(o1, o2);
//3.
Comparator<Integer> com3 = Integer::compare;
}

//Function中的R apply(T t)
//Math中的Long round(Double d)
@Test
public void test4(){
//1.
Function<Double, Long> fun1 = new Function<Double, Long>() {
@Override
public Long apply(Double aDouble) {
return Math.round(aDouble);
}
};
//2.
Function<Double,Long> fun2 = aDouble -> Math.round(aDouble);

//3.
Function<Double,Long> fun3 = Math::round;
}

Case3: 类::实例方法

要求:函数式接口中的抽象方法a与其内部实现时调用的对象的某个方法b的返回值类型相同。

同时,抽象方法a中有n个参数,方法b中有n-1个参数。且抽象方法a的第1个参数作为方法b的调用者,切抽象方法a的后n-1个参数与方法b的n-1个参数类型相同

注意:此方法b是非静态方法,需要对象调用。但是形式上,写出对象a所属的类

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
@Test
public void test6(){
//1.
BiPredicate<String, String> biPre1 = new BiPredicate<String, String>() {
@Override
public boolean test(String s, String s2) {
return s.equals(s2);
}
};

//2.
BiPredicate<String, String> biPre2 = (s1,s2) -> s1.equals(s2);

//3.
BiPredicate<String, String> biPre3 = String::equals;
}
@Test
public void test7(){
Employee emp = new Emplyoee("alugg",15);
//1.
Function<Employee, String> fun1 = new Function<Employee, String>() {
@Override
public String apply(Employee employee) {
return employee.getName();
}
};

//2.
Function<Employee, String> fun2 = emp -> emp.getName();

//3.
Function<Employee, String> fun3 = Employee::getName;
}

构造器引用

格式

类名::new

说明

  • 调用了类名对应的类中的某一个确定的构造器
  • 具体调用的是类中的哪一个构造器取决于函数式接口函数式接口的抽象方法的形参列表
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
@Test
public void test1(){
Supplier<Employee> sup1 = new Supplier<Employee>(){
@Override
public Employee get(){
return new Employee();
}
};

//2.构造器引用
Supplier<Employee> sup2 = Employee::new;
}

@Test
public void test2(){
//Function 中的R apply(T t)
Function<Integer, Employee> func1 = new Function<Integer, Employee>() {
@Override
public Employee apply(Integer id) {
return new Employee(id);
}
};

//2.
Function<Integer, Employee> func2 = Employee::new;
}

@Test
public void test3(){
BiFunction<Integer, String, Employee> func1 = new BiFunction<Integer, String, Employee>() {
@Override
public Employee apply(Integer id, String name) {
return new Employee(id, name);
}
};

//2.
BiFunction<Integer, String, Employee> func2 = Employee.new;
}

数组引用

1
2
3
4
5
6
7
8
9
10
11
@Test
public void test(){
Function<Integer, Employee[]> func1 = new Function<Integer, Employee[]>() {
@Override
public Employee[] apply(Integer length) {
return new Employee[length];
}
};
//
Function<Integer, Employee[]> func2 = Employee[]::new;
}

反射

反射是动态语言的关键,反射机制运行程序在运行期间借助于Reflection API取得任何类的内部信息,并能之间操作任意对象的内部属性与方法。

反射的意义:

1.面向对象中创建对象,调用指定结果(属性、方法)等功能,可以不适用反射,也可以使用反射。有什么区别?

使用反射,我们可以调用运行时类中任意的构造器、属性、方法。包括了私有属性、方法构造器

2.以前创建对象并调用方法的方式,与现在通过反射创建对象并调用方法的方式对比的画,那种使用的多?场景?

  • 作为程序员主要完成的是业务代码,对于相关的对象、方法的调用是确定的。因此使用非反射的情况多。

  • 因为反射提醒了动态性(可以在运行时动态的获取对象所属的类,动态的调用相关的方法),所以我们在设计框架时会大量使用反射。框架 = 注解+反射+设计模式

3.单例模式的饿汉式与懒汉式,私有化类的构造器。此时通过反射可以通过单例模式中类的多个对象吗?

是的

4.通过反射,可以调用类中私有结构,是否与面向对象的封装性有冲突?

封装性:提醒的是是否建议我们调用内部api的问题。比如,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
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
public class PersonTest {
@Test
public void test() throws InstantiationException, IllegalAccessException, NoSuchFieldException, InvocationTargetException, NoSuchMethodException {
//1.创建Person类的实例
Class<Person> clazz = Person.class;
Person p1 = clazz.newInstance();
System.out.println(p1);

//2.调用属性
Field ageField = clazz.getField("age");
ageField.set(p1, 10);
System.out.println(ageField.get(p1));

//3.调用方法
Method showMethod = clazz.getMethod("show");
showMethod.invoke(p1);
}

/*
* 出了Person类,就无法直接调用private修饰的结构
* 但可以通过反射的方式来调用
* */

@Test
public void test2() throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchFieldException {
//1.调用私有的构造器,创建Person的实例
//private Person(String name, int age)
Class clazz1 = Person.class;
Constructor cons = clazz1.getDeclaredConstructor(String.class, int.class);
cons.setAccessible(true);
Person p1 = (Person) cons.newInstance("Tom",12);
System.out.println(p1);

//2.调用私有属性
//private String name
Field nameField = clazz1.getDeclaredField("name");
nameField.setAccessible(true);
nameField.set(p1,"jerry");
System.out.println(nameField.get(p1));

//3.调用私有的方法
//private String showNation(String nation)
Method showNationMethod = clazz1.getDeclaredMethod("showNation", String.class);
showNationMethod.setAccessible(true);
String info = (String) showNationMethod.invoke(p1, "CHN");
System.out.println(info);

}
}
class Person{
private String name;
public int age;

public Person() {
}

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

public void show(){
System.out.println("showshow");
}

private String showNation(String nation){
return nation;
}

@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}

Class

针对于编写好的.java源文件进行编译(使用javac.exe),会生产一个或多个.class字节码文件。接着我们使用java.exe命令对知道的.class文件进行解释运行。这个解释运行的过程中,我们需要将.class字节码文件(使用类的加载器)加载到内存中。加载到内存中(方法区)的.class文件对应的结果即为Class的一个实例。

比如:加载到内存中的Person类或String类或User类。都作为Class的一个个实例

Class clazz1 = Person.class;//运行时类

Class clazz4 = Comparable.class;

说明:运行时类在内存会缓存起来,在整个执行期间,只会加载一次。

获取Class的实例的方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class ClassTest {
@Test
public void test() throws ClassNotFoundException {
//1.class
Class clazz1 = Person.class;
System.out.println(clazz1);

//2.调用运行时类的对象的getclass()
Person p1 = new Person();
Class clazz2 = p1.getClass();
System.out.println(clazz1 == clazz2);//true

//3.调用Class的静态方法forName(String className)
String className= "ex2.Person";
Class clazz3 = Class.forName(className);
System.out.println(clazz1==clazz3);//true

//4.使用类加载器的方式
Class clazz4 = ClassLoader.getSystemClassLoader().loadClass("ex2.Person");
System.out.println(clazz1==clazz4);//true
}
}

Class的实例可以指向哪些结构

所有java类型

类的加载过程

过程1:类的装载Load

将类的class文件读入内存,并为之创建一个java.lang.Class对象。此过程由类加载器完成

过程2:连接Link

  • 验证(Verify):确保加载的类信息符合JVM规范。
  • 准备(Prepare):正式为类变量份分配内存并设置类变量默认初始值的阶段,这些内存都会在方法区分配
  • 解析(Resolve):虚拟机常量池内的符号引用(常量名)替换为直接引用(地址)的过程

过程3:初始化initialization

执行类构造器()方法的过程。

类构造器()方法是由编译器自动收集类中所有类变量的赋值动作和静态代码块中的语句合并产生的。

类的加载器

作用:负责类的加载,并对应于一个Class的实例

分类(分为两种):

1.BootStrapClassLoader:引导类加载器

  • 使用C/c++语言编写,不能通过Java代码获取其实例
  • 负责加载Java的核心库(JAVA_HOME/jre/lib/rt.jar或sun.boot.class.path路径下的内容)

2.继承于ClassLoader的类加载器

  • ExtensionClassLoader:扩展类加载器
  • SystemClassLoader/ApplicationClassLoader:系统类加载器、应用程序类加载器
  • 用户自定义类加载器

以上的类加载器是否存在继承关系

不存在。

通过classLoader加载指定配置文件

1
2
3
4
5
6
7
8
9
10
@Test
public void test2() throws IOException {
Properties pros = new Properties();
//通过类的加载器读取的文件默认路径为:当前module的src下
InputStream is = ClassLoader.getSystemClassLoader().getResourceAsStream("../info.properties");
pros.load(is);
String name = pros.getProperty("name");
String pwd = pros.getProperty("password");
System.out.println(name+":"+pwd);
}

应用

1.创建运行时的对象

1.1 实现方法:

通过Class的实例调用newInstance()方法即可

1
2
3
4
5
6
7
8
9
10
public class NewInstanceTest {
@Test
public void test1() throws InstantiationException, IllegalAccessException {
Class clazz = Person.class;

//创建Person类的实例
Person per = (Person) clazz.newInstance();
System.out.println(per);
}
}

1.2 创建对象成功的满足条件:

  1. 要求运行时类中必须提供一个空参的构造器

  2. 要求提供的空参构造器的权限要足够

1.3 JavaBean中要给一个当前类提供一个公共的空参构造器的意义

场景1:子类对象在实例化时,子类的构造器的首行默认调用父类空参的构造器。

场景2:在反射中,经常用来创建运行时类的对象。那么我要求各个运行时类都提供一个空参的构造器,便于我们编写创建运行时类对象的代码

1.4在jdk9中标识为过时,替换成什么结构

通过Constructor类调用newInstance(…)

2.获取运行时类的内部结构

2.1获取运行时类的内部结果1:所有属性、所有方法、所有构造器

2.2获取运行时类的内部结果2:父类、接口、包、带泛型的父类、父类的泛型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Test
public void test2() throws ClassNotFoundException {
Class clazz = Class.forName("com.alugg.reflection.Person");
//运行时类的父类
Type superclass = clazz.getSuperclass();

//运行时类的接口
Class[] interfaces = clazz.getInterfaces();
for(Class c:interfaces){
System.out.println(c);
}
//运行时类的包
Package pack = clazz.getPackage();
//运行时类的父类的泛型
Type superclass1 = clazz.getGenericSuperclass();
//如果父类是带泛型的,则可以强转为Parameterized
ParameterizedType paramType = (ParameterizedType) superclass1;
//调用getActualTypeArguments()获取带泛型的参数,结果是一个数组,因为可能有多个泛型参数
Type[] arguments = paramType.getActualTypeArguments();
System.out.println(((Class)arguments[0]).getName());
}

3.调用指定的结构:指定的属性、方法、构造器

3.1调用指定属性:

实例属性

1
2
3
4
5
6
7
8
9
10
11
12
13
public void test3() throws InstantiationException, IllegalAccessException, NoSuchFieldException {
Class clazz = Person.class;
Person per = (Person)clazz.newInstance();

//1.通过Class实例调用getDeclaredField(String fieldName)获取运行时类指定的属性
Field nameField = clazz.getDeclaredField("name");
//2.setAccessible(true),确保此属性是可以访问的
nameField.setAccessible(true);
//3.获取或设置此属性的值
nameField.set(per,"alugg");
System.out.println(nameField.get(per));

}

静态类属性

1
2
3
4
5
6
7
8
9
10
11
12
13
//private static String info;
@Test
public void test3() throws InstantiationException, IllegalAccessException, NoSuchFieldException {
Class clazz = Person.class;

//1.通过Class实例调用getDeclaredField(String fieldName)获取运行时类指定的属性
Field infoField = clazz.getDeclaredField("info");
//2.setAccessible(true),确保此属性是可以访问的
infoField.setAccessible(true);
//3.获取或设置此属性的值
infoField.set(Person.class,"xixixhaha");
System.out.println(infoField.get(Person.class));
}

3.2调用方法

实例方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//private String showNation(String nation, int age)
@Test
public void test4() throws InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
Class clazz = Person.class;
Person per = (Person) clazz.newInstance();

//1.通过Class的实例调用getDeclaredMthod(String methodName, Class ... args),获取指定的方法
Method showNationMethod = clazz.getDeclaredMethod("showNation", String.class, int.class)
//2.setAccessible(true):确保此方法是可访问的
showNationMethod.setAccessible(true);
//3.通过method实例调用invoke(Object obj, Object .., objs),即对Method对应的方法的调用
Object returnValue = showNationMethod.invoke(per,"chnxxx");
System.out.println(returnValue);
}

类静态方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
//public static void showInfo()
@Test
public void test4() throws InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
Class clazz = Person.class;
//1.通过Class的实例调用getDeclaredMthod(String methodName, Class ... args),获取指定的方法
Method showNationMethod = clazz.getDeclaredMethod("showInfo");
//2.setAccessible(true):确保此方法是可访问的
showNationMethod.setAccessible(true);
//3.通过method实例调用invoke(Object obj, Object .., objs),即对Method对应的方法的调用
//如果Method对应的方法的返回值为void,则invoke()返回值为null
Object returnValue = showNationMethod.invoke(null);
System.out.println(returnValue);
}

3.3调用构造器

1
2
3
4
5
6
7
8
9
10
11
//private Person(String name, int age)
@Test
public void test5() throws NoSuchMethodException {
Class clazz = Person.class;
//1.通过Class的实例调用getDeclaredConstructor(Class ... args) 获取指定参数类型的构造器
Constructor constructor = clazz.getDeclaredConstructor(String.class, int.class);
//2.setAccessible(true):确保此构造器是可以访问的
constructor.setAccessible(true);
//3.通过Constructor实例调用newInstance(Object ... objs),返回一个运行时类的实例。
Person tom = (Person) constructor.newInstance("TOM", 12);
}

4.获取注解信息

1
2
3
4
5
6
7
8
9
10
11
12
@Test
public void test1() throws NoSuchFieldException {
Class clazz = Customer.class;
Table annotation = (Table) clazz.getDeclaredAnnotation(Table.class);
System.out.println(annotation.value());

//获取属性声明的注解
Field nameField = clazz.getDeclaredField("name");
Column nameColumn = nameField.getDeclaredAnnotation(Column.class);
System.out.println(nameColumn.columnName());
System.out.println(nameColumn.columnType());
}

常见问题

反射的好处,为什么需要反射,使用的场合

实现反射的类有哪些?

反射是如何实现?

Class类的作用和生成Class对象有哪些

Class.forName()会调用哪些方法?会调用构造方法吗?加载的类放在哪?

Class.forName()会执行执行类构造器()方法

不会调用构造方法

加载的类放在方法区

类的加载流程

创建对象有几种方法

Java反射创建效率高还是new创建高?

new高

如何利用反射机制访问类的方法或获取私有属性

InetAddress的使用

作用

InetAddress类的一个实例就代表一个具体的ip地址

实例化方式

InetAddress getByName(String host):获取指定ip对应的InetAddress的实例

InetAddress getLocalHost():获取本地ip对应的InetAddress的实例

常用方法

getHostName():

getHostAddress()

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 netTest {
@Test
public void test(){
try{
//1.实例化
//getByName(String host):获取指定ip对应的InetAddress的实例
InetAddress inet1 = InetAddress.getByName("192.168.23.31");
System.out.println(inet1);

InetAddress inet2 = InetAddress.getByName("www.baidu.com");
System.out.println(inet2);

//getLocalHost():获取本地Ip对应的InetAddress的实例
InetAddress inet3 = InetAddress.getLocalHost();
System.out.println(inet3);

//2.两个常用方法
System.out.println(inet1.getHostName());
System.out.println(inet1.getHostAddress());

}catch (UnknownHostException e){
e.printStackTrace();
}

}
}

Socket类

TCP

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
public class TCPTest2 {
@Test
public void client(){
Socket socket = null;
FileInputStream fis = null;
OutputStream os = null;
try {
//1.创建Socket
InetAddress inetAddress = InetAddress.getByName("127.0.0.1");
int port = 9090;
socket = new Socket(inetAddress, port);
//2.创建File的实例、FileInputStream的实例
File file = new File("xx.jpg");
fis = new FileInputStream(file);
//3.通过Socket获取输出流
os = socket.getOutputStream();
//读写数据
byte[] buffer = new byte[1024];
int len;
while ((len=fis.read(buffer))!=-1){
os.write(buffer,0,len);
}
//4.接收来自于服务端的数据
InputStream is = socket.getInputStream();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buffer1 = new byte[5];
int len1;
while((len1 = is.read(buffer))!=-1){
baos.write(buffer1,0,len);
}
System.out.println(baos.toString());
//客户端不再继续发送数据
socket.shutdownOutput();

} catch (IOException e) {
throw new RuntimeException(e);
} finally {
//5.关闭Socket和相关流
try {
os.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
try {
fis.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
try {
socket.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}

}

@Test
public void server(){
ServerSocket serverSocket = null;
Socket socket = null;
InputStream is = null;
FileOutputStream fos = null;
try {
//创建ServerSocket
int port = 9090;
serverSocket = new ServerSocket();
//2.接受来自于客户端的socket:accept(_
socket = serverSocket.accept();
//3.通过Socket获取一个输入流
is = socket.getInputStream();
//4.创建File类的实例、FileOutputStream
File file = new File("xxx_copy.jpg");
fos = new FileOutputStream(file);
//5.读写过程
byte[] buffer = new byte[1024];
int len;
while((len = is.read(buffer))!=-1){
fos.write(buffer, 0, len);
}
//6.服务端发送数据给客户端
OutputStream os = socket.getOutputStream();
os.write("pic recevied".getBytes());

} catch (IOException e) {
throw new RuntimeException(e);
} finally {
//6关闭相关Socket和流
try {
fos.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
try {
is.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
try {
socket.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
try {
serverSocket.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}

}
}

URL

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class UrlTest {
@Test
public void test(){
String str = "http://www.google.com";
try {
URL url = new URL(str);

System.out.println(url.getProtocol());
System.out.println(url.getHost());
System.out.println(url.getPort());
System.out.println(url.getFile());

} catch (MalformedURLException e) {
throw new RuntimeException(e);
}
}
}

File类

File类的理解

  • File类位于java.io包下。
  • File类的一个对象,对应于操作系统下的一个文件或一个文件目录
  • File类中声明了新建、删除、获取名称、重命名等方法,并没有涉及到文件内容的读写操作。要想实现文件内容的读写,需要使用io流。
  • File类的对象,通常是作为io流操作的文件的端点出现。
    • 代码层面,将File类对象作为参数传递到IO流相关类的构造器中。

构造器

  • File(String pathname) :以pathname为路径创建File对象,可以是绝对路径或者相对路径
  • File(String parent, String child):以parent为父路径,child为子路径创建file对象
  • File(File parent, String child) 根据一个父File对象和子文件路径创建file对象
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
public class FileTest {
@Test
public void test1(){
/* public File(String pathname)
方式1:绝对路径:windows操作系统为例,包括盘符在内的文件或文件目录的完整路径
方式2:相对路径:相对于某一个文件目录的相对位置
在IDEA中:
单元测试方法:相对于当前Module来讲
main方法:相对于当前project来讲

*/
File file1 = new File("d:/io\\hello.txt");
File file2 = new File("abc");
}
@Test
public void test2(){
// public File(String parent, String child)
// 参数1:一定是一个文件目录(String方式),参数2:可以是一个文件,也可以是一个文件目录
File file1 = new File("d:\\io","abc.txt");
File file2 = new File("abc","ab");
//参数1:一定是一个文件目录,参数2:可以是一个文件,也可以是一个文件目录
// public File(File parent, String child)
File file3 = new File(file2,"ab");
}
}

方法

获取文件和目录基本信息

  • String getName():获取名称
  • String getPath():获取路径
  • String getAbsolutePaht():获取绝对路径
  • File getAbsoluteFile():获取绝对路径表示的文件
  • String getParent():获取上层文件目录路径。不能获取目录的长度
  • long length():获取文件长度(字节数).不能获取目录的长度
  • long lastModified():获取最后一次的修改时间,毫秒值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Test
public void test(){
File file = new File("hello.txt");
System.out.println(file.getName());//hello.txt
System.out.println(file.getPath());//hello.txt 因为是相对路径
System.out.println(file.getAbsolutePath());//D:\javaSE\JavaSE\Chapter15_file\hello.txt
System.out.println(file.getAbsoluteFile());////D:\javaSE\JavaSE\Chapter15_file\hello.txt返回的是对象
System.out.println(file.getParent());//null 因为是相对路径
System.out.println(file.length());//6
System.out.println(file.lastModified());//1697208159548时间戳

File file1 = new File("D:\\javaSE\\Chapter15_file\\hello.txt");
System.out.println(file1.getName());//hello.txt
System.out.println(file1.getPath());//D:\javaSE\Chapter15_file\hello.text
System.out.println(file1.getAbsolutePath());//D:\javaSE\Chapter15_file\hello.text
System.out.println(file1.getAbsoluteFile());//D:\javaSE\Chapter15_file\hello.text
System.out.println(file1.getParent());//D:\javaSE\Chapter15_file
System.out.println(file1.length());
System.out.println(file1.lastModified());
}

列出目录的下一级

  • String[] list():返回一个String数组,表示该File目录中的所有子文件或目录。
  • File[] listFiles():返回一个File数组,表示该File目录中的所有子文件或目录。
1
2
3
4
5
6
7
8
9
10
11
12
13
@Test
public void test2(){
File file1 = new File("D:\\javaSE\\javaSE");
String[] fileArr = file1.list();
for(String s:fileArr){
System.out.println(s);
}

File[] files = file1.listFiles();
for(File f:files){
System.out.println(f);
}
}

重命名

  • file1.renamTo(file2):把文件重命名为指定的文件路径。file1必须存在且file2不存在
1
2
3
4
5
6
7
8
@Test
public void test3(){
File file1 = new File("hello.txt");
File file2 = new File("D:\\javaSE\\JavaSE\\Chapter15_file\\hello2.txt");

boolean renameSuccess = file1.renameTo(file2);
System.out.println();
}

判断功能的方法

  • boolean exits():此File表示的文件或目录是否实际存在
  • boolean isDirectory():此File表示的是否为目录
  • boolean isFile():此File表示的是否为文件
  • boolean canRead():判断是否可读
  • boolean canWrite():判断是否可写
  • boolean isHidden():判断是否隐藏
1
2
3
4
5
6
7
8
9
10
@Test
public void test4(){
File file1 = new File("hello2.txt");
System.out.println(file1.exists());
System.out.println(file1.isDirectory());
System.out.println(file1.isFile());
System.out.println(file1.canRead());
System.out.println(file1.canWrite());
System.out.println(file1.isHidden());
}

创建和删除类型

  • boolean createNewFile():创建文件。若文件存在,则不创建,返回false
  • boolean mkdir():创建文件目录。如果此文件目录存在,就不创建了。如果此文件目录的上册目录不存在,也不创建
  • boolean mkdirs():创建文件目录。如果上层文件目录不存在,一并创建
  • boolean delete():删除文件或文件夹
  • 注意:java中的删除不走回收站。要删除一个文件目录,请主要该文件目录内不能包含文件或者文件目录

IO流

java中对于数据的输入/输出操作以“流”的方式进行,可以看作是一种数据的流动

流的分类

  • 流向的不同:输入流、输出流
  • 处理单位的不同:字节流、字符流
  • 流的角色的不同:节点流、处理流

XaZad.png

流的API

IO流共涉及40多个类,实际上非常规则,都是从如下4个抽象基类派生的。

(抽象基类) 输入流 输出流
字节流 InputStream OutputStream
字符流 Reader Writer

节点流

字符流

FileReader\FileWriter的使用

执行步骤:

  1. 创建读或写的File类对象
  2. 创建输入流或输出流
  3. 具体的读入或写出的过程
    1. 读入:read(char[] cbuffer)
    2. 写出:write(String str)/ write(char[] cbuffer,0,len)
  4. 关闭流资源

注意点:

  1. 因为涉及流资源关闭操作,所以出现异常的花,需要使用try-catch-finally的方式来处理异常

  2. 对于输入流来讲,要求File类的对象对应的物理磁盘上的文件必须存在。否则会报FileNotFoundException

    对于输出流来讲,File类的对象对应的物理磁盘上的文件可以不存在。

    • 如果此文件不存在,则在输出的过程中,会自动创建此文件,并写出数据到此文件中。
    • 如果此文件存在,使用FileWriter(file file)或FileWriter(File file, false):输出数据过程中,会创建同名的文件对现有的文件进行覆盖。
    • FileWriter(File file, true):输出数据过程中,会在现有的文件的末尾追加写出的内容。

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
@Test
public void test() throws IOException {
FileReader fileReader = null;
try {
//1.创建File类的对象,对应着hello.txt文件
File file1 = new File("hello.txt");

//2.创建输入型的字符流,用于读取数据
fileReader = new FileReader(file1);

//3.读取数据,并显示在控制台
//方式2.read(char[] cbuffer)
char[] cbuffer = new char[5];
int len;
while((len=fileReader.read(cbuffer))!= -1){
for(int i =0; i<len;i++){
System.out.println(cbuffer[i]);
}
}

} catch (IOException e) {
throw new RuntimeException(e);
} finally {
//4.流资源的关闭操作
try {
if(fileReader != null){
fileReader.close();
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

/*
* 需求:将内存中的数据写出到指定的文件中
*
* */
@Test
public void test2() {
FileWriter fw = null;
try {
//1.创建File类的对象,指明要写出的文件的名称
File file = new File("info.txt");
//2.创建输出流
fw = new FileWriter(file,false);//append是否在原文件追加

//3.写出的具体过程
//输出的方法:write(String str)/write(char[] cdata)
fw.write("I love");
fw.write("you love");
fw.write("xixi");
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
try {
if(fw!=null){
fw.close();
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
/*
* 需求:复制一份hello.txt文件,命名为hello_copy.txt
* */
@Test
public void test1() {
FileReader fr = null;
FileWriter fw = null;
try {
//1.创建File类的对象
File srcFile = new File("hello.txt");
File destFile = new File("hello_copy.txt");

//2.创建输入流、输出流
fr = new FileReader(srcFile);
fw = new FileWriter(destFile);

//3.数据的读入和写出的过程
char[] cbuffer = new char[5];
int len;//记录每次读入到cbuffer中的字符个数
while ((len = fr.read(cbuffer)) != -1) {
fw.write(cbuffer, 0, len);
}
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
try {
if(fw!=null){
fw.close();
}
} catch (IOException e) {
throw new RuntimeException(e);
}
try {
if(fr!=null) {
fr.close();
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

字节流

FileInputStream\FileOutputStream

使用步骤

  1. 创建读或写的File类对象
  2. 创建输入流或输出流
  3. 具体的读入或写出的过程
    1. 读入:read(byte[] buffer)
    2. 写出:write(byte[] buffer,0,len)
  4. 关闭流资源

注意点

  • 在文件流的注意点基础之上。 字符流只能用来操作文本文件,不能用来处理非文本文件。
  • 对于字节流,通常是用来处理非文本文件。但是涉及到文本文件的复制操作,也可以使用字节流。

说明:

文本文件:.txt、 .java、.c

非文本文件:.doc 、 .xls 、.jpg、.pdf 、.mp3

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
public class FileStreamTest{
@Test
public void test() {
FileInputStream fis = null;
FileOutputStream fos = null;
try {
//1.创建相关的File类的对象
File srcFile = new File("xxx.jpg");
File desFile = new File("xxx_copy.jpg");

//2.创建相关的字节流
fis = new FileInputStream(srcFile);
fos = new FileOutputStream(desFile);

//3.数据读取和写出
byte[] buffer = new byte[1024]; //1kb
int len;
while((len = fis.read(buffer))!=-1){
fos.write(buffer,0,len);
}

} catch (IOException e) {
throw new RuntimeException(e);
} finally {
//4.关闭资源
try {
if(fos!=null){
fos.close();
}
} catch (Exception e) {
throw new RuntimeException(e);
}
try {
if(fos!=null){
fis.close();
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
}

缓冲流

抽象基类 节点流(文件流) 缓冲流
InputStream FileInputStream BufferedInputStream
OutputStream FileOutputStream BufferedOutputStream
Reader FileReader BufferedReader
Writer FileWriter BufferedWriter

缓冲流的作用

减少与磁盘的交互,提升文件读写的效率。

缓冲区:内部提供了一个数组,将读取或写出的数据,现在此数组中缓冲。达到一定程度时,集中性的写出。

4个缓冲流

处理非文本文件的字节流: 方法

  • BufferedInputStream read(byte[] buffer)

  • BufferedOutputStream write(byte[] buffer,0,len)

处理文本文件的字符流

  • BufferedReader read(char[] cbuffer) / readLine()
  • BufferedWriter write(char[] cbuffer, 0, len)

实现步骤

  1. 创建File对象、流的对象(包括文件流、缓冲流)

  2. 使用缓冲流实现读取数据或写出数据的过程

    ​ 读取:int read(char[] cbuf/byte[] buffer) :每次将数据读入到cbuf/buffer数组中,并返回读入到数组中

    ​ 写出: void write(String str)/write(char[] cbuf):将str或cbuf写出到文件中

    ​ void write(byte[] buffer) 将byte写出到文件中

  3. 关闭资源

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 BufferInputTest {
@Test
public void test() {
FileInputStream fis = null;
FileOutputStream fos = null;
BufferedInputStream bis = null;
BufferedOutputStream bos = null;
try {
//1.创建相关的File类的对象
File srcFile = new File("xxx.jpg");
File desFile = new File("xxx_copy.jpg");

//2.创建相关的字节流、缓冲流
fis = new FileInputStream(srcFile);
fos = new FileOutputStream(desFile);

bis = new BufferedInputStream(fis);
bos = new BufferedOutputStream(fos);

//3.数据读取和写出
byte[] buffer = new byte[1024]; //1kb
int len;
while((len = bis.read(buffer))!=-1){
bos.write(buffer,0,len);
}

} catch (IOException e) {
throw new RuntimeException(e);
} finally {
//4.关闭资源
//外层流的关闭会自动关闭内存流
try {
if(bis!=null){
bis.close();
}
} catch (Exception e) {
throw new RuntimeException(e);
}
try {
if(bos!=null){
bos.close();
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
}

转换流

字符编码:字符、字符串、字符数组–》字节、字节数组

字符解码:字节、字节数组–》字符、字符串、字符数组

如果希望程序在读取文本时,不出现乱码

解码时使用的字符集必须与当初编码时使用的字符集一致

解码集必须要与编码集兼容。

作用:实现字节与字符之间的转换

API

InputStreamReader:将一个输入型的字节流转换为输入型的字符流

OutputStreamWriter:讲一个输出型的字符流转换为输出型的字节流

关于字符集的理解

  • ascii:主要用来存储a,b,c英文字符和1,2,3常用标点字符。每个字符1个字节。

  • iso-8859-1:了解,每个字符占用1个字符。向下兼容ascii

  • gbk:用来存储包括中文简体繁体,a,b,c等英文字符和1,2,3,常用的标点符号等字符。中文字符使用2个字节,向下兼容ascii。英文1个字节。

  • utf-8:可以用来存储世界范围内主要的语言的所有的字符。使用1-4个不等的字节表示一个字符。中文使用3个字节,英文1个。

内存中的字符

一个字符(char)占用2个字节。在内存使用的字符集称为Unicode字符集。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Test
public void test2() throws IOException{
//1.造文件
File file1 = new File("xxx_gbk.txt");
File file2 = new File("xxx_gbk2utf8");
//2.造流
FileInputStream fis = new FileInputStream(file1);
InputStreamReader isr = new InputStreamReader(fis,"GBK");

FileOutputStream fos = new FileOutputStream(file2);
OutputStreamWriter oos = new OutputStreamWriter(fos,"utf8");

//3.读写过程
char[] cBuffer = new char[1024];
int len;
while((len = isr.read(cBuffer))!=-1){
oos.write(cBuffer,0,len);
}
System.out.println("操作完成");
//4.关闭资源

oos.close();
isr.close();
}

对象流

说明

考虑将内存中定义的变量保存在文件中

数据流(只支持基本数据类型和字符串的读写,而不支持其他jav对象的类型)

  • DataOutputStream:将内存中基本数据类型、String类型的变量写入具体的文件中
  • DataInputStream:将文件中保存的数据还原为内存的基本数据类型、String类型的变量。

对象流

  • ObjectOutputStream:将java基本数据类型和对象写入字节输出流中。通过在流中使用文件可以实现java各种基本数据类型的数据以及对象的持久存储
  • ObjectInputStream:ObjectInputStream对以前使用ObjectOutputStream写出的基本数据类型的数据和对象进行读入操作,保存在内存中。

对象的序列化机制

对象序列化机制允许把内存中java对象转换成平台无关的二进制流,从而允许把这种二进制流持久地保存在磁盘上,或通过网络将这种二进制流传输到另一个网络节点。//当其他程序获取了这种二进制流,就可以恢复成原来的java对象。

  • 序列化过程:ObjectOutputStream流实现。将内存中的java对象保存在文件中或通过网络传输出去
  • 反序列化过程:使用ObjectInputStream实现。将文件中的数据或网络传输过来的数据还原为内存中的java的对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class ObjectOutputTest {
@Test
public void test1() throws IOException {
File file = new File("xx.txt");
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file));
oos.writeUTF("xixihaha");
oos.writeObject("xixihaha");
oos.flush();
oos.close();
}

@Test
public void test2() throws IOException, ClassNotFoundException {
File file = new File("xx.txt");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
String str1 = ois.readUTF();
String str2 = (String) ois.readObject();
System.out.println(str1);
System.out.println(str2);

ois.close();
}
}

自定义类实现序列化的要求

  • 自定义类需要实现接口:Serializable
  • 要求自定义类声明一个全局常量:static final long serialVersionUID = 42234234L;用于唯一的标识当前的类
  • 要求自定义类的各个属性必须是可序列化的。
    • 对于基本数据类型的属性:默认可序列化
    • 对于引用数据类型:需要实现Serializable接口

注意点:

  1. 如果不声明全局常量,系统会自动声明生产一个针对于当前类的serialVersionUID。

    如果修改此类的话,会导致serialVersionUID变换,进而导致反序列化时,出现InvalidClassException异常

  2. 类中的属性如果声明为transient或static,则不会序列化。

其他流

标准输入流、标准输出流、system.in、system.out