Qeuroal's Blog

静幽正治

前端

  • HTML:网页基本结构

  • CSS:美化网页

  • JS:使得网页动起来产生交互性行为

初识HTML

什么是HTML

Hyper Text Markup Language(超文本标记语言)

  • 超文本:文字、图片、音频、视频、动画等

W3C标准

w3c: 万维网联盟

标准:

  • 结构化标准语言(HTMLXML)
  • 表现标准语言(css
  • 行为标准 (DOMECMAScript

基本信息

  • 注释:<!-- 注释 -->
  • <!DOCTYPE html>:告诉浏览器,我们要使用什么规范
  • <html> </html>: 总标签,只有在里面写才有意义
  • head标签:网页头部
  • body标签:网页主体
  • title标签:网页标题
  • meta 标签:描述性标签,用来描述网站的一些信息,可以用来被搜索到;一般用来做 SEO

基本标签

标题标签

1
2
3
4
5
6
<h1>1级标签</h1>
<h2>2级标签</h2>
<h3>3级标签</h3>
<h4>4级标签</h4>
<h5>5级标签</h5>
<h6>6级标签</h6>

段落标签

1
2
3
<p>hello world</p>
<p>hello world2</p>
<p>hello world3</p>

换行标签

即便换行了,也是一个段落

1
2
3
hello world <br/>
hello world2 <br/>
hello world3 <br/>

水平线标签

1
<hr/>

字体样式标签

  • 粗体斜体

    1
    2
    粗体:<strong>Hello html</strong>
    斜体:<em>Hello html</em>

注释和特殊符号

注释

1
<!--注释-->

特殊符号

  • 空格:空 格:空&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;格

  • 大于号:&gt;

  • 小于号:&lt;

  • 版权符号:&copy;

  • 记忆方式:

    • &字母:可以直接调
    • 百度查(不推荐)

图像标签

常见图像格式

  • JPG
  • GIF
  • PNG
  • BMP:位图
  • ……

嵌入图片

1
2
3
4
5
6
7
8
<!--
src:图片地址(必填):
绝对地址:E:\SourceCode\HTMLSpace\learnProject\resources\images\pdx.jpg
相对路径:../resources/images/pdx.jpg

alt: 图片名字(必填)
-->
<img src="../resources/images/pdx.jpg" alt="Qeuroal" title="悬停文字" width="300" height="480">

链接标签

可以点的东西都是链接

页面间跳转:从一个页面跳转到另一个页面

文本超链接

1
2
3
4
5
6
7
8
<!--a标签
href:必填,表示要跳转到哪个页面
target:表示窗口在哪里打开
_blank: 在新标签页中打开
_self:在自己的网页打开
-->
<a href="test1.html" target="_blank">点击我跳转test1</a>
<a href="https://qeuroal.top/" target="_self">点击我跳转到主页</a>

图像超链接

1
2
3
<a href="test1.html">
<img src="../resources/images/pdx.jpg" alt="Qeuroal" title="点击我跳转test1" width="300" height="480">
</a>

页面内跳转:页面内跳转

锚链接

1
2
3
4
5
6
7
8
9
10
<!--使用name作为标记-->
<a name="top">顶部</a>

<!--锚链接:页面内跳转
1. 需要一个锚标记
2. 跳转到标记
3. #: 通过#跳到标记
-->
<a href="#top">回到顶部</a>
<a href="链接标签.html#down">跳转</a>

功能性连接

邮件链接

1
2
3
4
<!--功能性连接
邮件链接:mailto
-->
<a href="mailto:xxxxx@163.com"></a>

qq链接

1
2
3
<a target="_blank" href="http://wpa.qq.com/msgrd?v=3&uin=&site=qq&menu=yes">
<img border="0" src="http://wpa.qq.com/pa?p=2::53" alt="你好,加我领取资料" title="你好,加我领取资料"/>
</a>

块元素和行内元素

块元素

  • 无论内容多少,该元素独占一行
  • ph1-h6、…)

行内元素

  • 内容撑开宽度,左右都是行内元素的可以排在一行
  • astrongem、…)

列表标签

什么是列表

列表的分类

有序列表

1
2
3
4
5
6
7
8
9
10
<!--有序列表
应用:试卷、问答……
-->
<ol>
<li>java</li>
<li>python</li>
<li>运维</li>
<li>前端</li>
<li>c/c++</li>
</ol>

无序列表

1
2
3
4
5
6
7
8
9
10
<!--无序列表
应用:导航、侧边栏……
-->
<ul>
<li>java</li>
<li>python</li>
<li>运维</li>
<li>前端</li>
<li>c/c++</li>
</ul>

自定义列表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!--自定义列表
dl:标签
dt:列表名称
dd:列表选项

应用:公司网站底部
-->
<dl>
<dt>学科</dt>
<dd>java</dd>
<dd>python</dd>
<dd>Linux</dd>
<dd>C</dd>

<dt>位置</dt>
<dd>唐山</dd>
<dd>石家庄</dd>
</dl>

表格标签

为什么使用表格

  • 简单通用
  • 结构稳定

基本结构

  • 单元格
  • 跨行
  • 跨列

实现

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
<!--表格table
行:tr
列:td
-->
<table border="1px">
<tr>
<!--colspan 跨列-->
<td colspan="4">1</td>
<td>2</td>
<td>3</td>
<td>123</td>

</tr>
<tr>
<!--colspan 跨行-->
<td rowspan="2">4</td>
<td>5</td>
<td>6</td>
</tr>
<tr>
<td>7</td>
<td>8</td>
<td>9</td>
</tr>
</table>

媒体元素

视频元素video

1
2
3
4
5
6
<!--视频
src: 资源路径
controls: 控制条
autoplay: 自动播放
-->
<video src="../resources/video/起风了.mp4" controls autoplay></video>

音频元素audio

1
2
<!--音频-->
<audio src="../resources/audio/起风了.mp3" controls autoplay></audio>

页面结构分析

1
2
3
4
5
6
7
8
9
10
11
<header>
<h2>网页头部</h2>
</header>

<section>
<h2>网页主体</h2>
</section>

<footer>
<h2>网页脚部</h2>
</footer>

iframe内联框架

1
2
3
4
5
6
7
8
<!--iframe内联框架
src: 地址
frameborder:
w-h:宽度-高度
-->
<iframe src="https://www.baidu.com" name="baidu" frameborder="0" width="1000px" height="800px"></iframe>

<a href="https://qeuroal.top" target="baidu">点击跳转</a>

表单语法

规定

  • input标签 都要有一个name

表单元素格式

文本输入框

1
2
3
4
5
6
7
8
<!--文本输入框:input type="text"
value="Qeuroal" 默认初始值
maxlength="8" 最长能写几个字符
size="30" 文本框的长度
-->
<p>用户名:<input type="text" placeholder="请输入用户名" name="username" ></p>
<!--密码框:input type="password"-->
<p>密码:<input type="password" placeholder="请输入密码" name="pwd"></p>

单选框

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!--单选框
input type="radio"
value: 单选框的值
name: 表示组,组一样才能选择一个
-->
<p>性别:
<input type="radio" value="boy" name="sex" checked/>
<input type="radio" value="girl" name="sex"/>
</p>

<p>
<input type="submit">
<input type="reset">
</p>

多选框

1
2
3
4
5
6
7
8
9
<!--多选框
input type="checkbox"
-->
<p> 爱好:
<input type="checkbox" value="sleep" name="hobby">睡觉
<input type="checkbox" value="chat" name="hobby">聊天
<input type="checkbox" value="code" name="hobby" checked>码代码
<input type="checkbox" value="game" name="hobby">游戏
</p>

按钮

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!--
按钮
input type="button" 普通按钮
input type="image" 图像按钮
input type="submit" 提交按钮
input type="reset" 充值
-->
<p>按钮:
<input type="button" name="btn1" value="点击变长">
<input type="image" name="imgBtn1" src="../resources/images/pdx.jpg">
</p>

<p>
<input type="submit">
<input type="reset" value="清空表单">
</p>

下拉框(列表框)

1
2
3
4
5
6
7
8
9
10
<!--下拉框,列表框
-->
<p>国家:
<select name="countries">
<option value="china">中国</option>
<option value="eth" selected>瑞士ETH</option>
<option value="us">美国</option>
<option value="dg">德国</option>
</select>
</p>

文本域

1
2
3
<p>反馈 <br>
<textarea name="textarea" cols="30" rows="10" placeholder="请输入反馈信息"></textarea>
</p>

文件域

1
2
3
4
<p>
<input type="file" name="files">
<input type="button" value="上传" name="upload">
</p>

搜索框及简单验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<!--邮件验证-->
<p> 邮箱:
<input type="email" name="email">
</p>

<!--url-->
<p> url:
<input type="url" name="url">
</p>

<!--数字验证-->
<p> 数字:
<input type="number" name="num" max="100" min="0" step="1">
</p>

<!--滑块-->
<p> 音量:
<input type="range" name="voice" min="0" max="100">
</p>

<!--搜索框-->
<p> 搜索:
<input type="search" name="search">
</p>

表单的应用

只读 readonly

1
<p>用户名:<input type="text" placeholder="请输入用户名" name="username" value="admin" readonly></p>

禁用 disabled

1
<input type="radio" value="boy" name="sex" checked disabled/>

隐藏域 hidden

1
<p>密码:<input type="password" placeholder="请输入密码" name="pwd" hidden></p>

效果

增强鼠标可用性

1
2
3
4
<p>
<label for="mark">你点我试试</label>
<input type="text" id="mark">
</p>

表单初级验证

为什么要进行表单验证

  • 减轻服务器的压力
  • 保证数据的安全性

常用方式

  • placeholder :提示信息

    1
    <textarea name="textarea"  cols="30" rows="10" placeholder="请输入反馈信息"></textarea>
  • required:非空判断

    1
    <input type="search" name="search" required>
  • pattern:正则表达式

    • 如何查:常用正则表达式点击这里

    • 使用:

      1
      2
      3
      4
      <!--自定义邮箱-->
      <p>自定义邮箱
      <input type="email" name="diyMail" pattern="\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*">
      </p>

总结

第一个flask程序

Flask(__name__)

1
2
3
4
5
# 初始化一个Flask对象
# Flask()
# 需要传递一个参数 __name__
# 1. 方便flask框架去寻找资源
# 2. 方便flask插件比如Flask-Sqlalchemy出现错误的时候,好去寻找问题所在的位置

@app.route()

1
2
3
4
5
6
7
# @app.route是一个装饰器(可以看成url的后缀:127.0.0.1:5000/login 的 /login)
# @开头,并且在函数的上面,说明是装饰器
# 这个装饰器的作用是做一个url和视图函数(习惯性的命名,就是这里的hello_world函数)的映射
# 127.0.0.1:5000 -> 去请求hello_world这个函数然后将结果返回给浏览器
@app.route('/')
def hello_world():
return “我是第一个flask程序”

app.run():

启动一个应用服务器,来接收用户的请求。

类似于:

1
2
while (True):
listen()

设置debug模式

  1. app.run() 中传入一个关键字参数debugapp.run(debug=True), 就设置当前项目为 debug 模式
  2. debug 模式的两大功能
    • 当程序出现问题时,可以在也米那种看到错误信息和出错的位置
    • 只要修改了项目中的 Python 文件,程序会自动加载,不需要手动重新启动服务器

使用配置文件

  1. 新建一个 .py文件,这里设置为 config.py

  2. 然后设置大写,如:DEBUG=True

  3. app进行关联

    1
    2
    3
    4
    # 假设配置文件为config
    import config

    app.config.from_object(config)
  4. 还有许多的其他参数,都是放在这个配置文件中,比如 SECRET_KEYSQLALCHEMY 这些配置都是在这个文件中

URL传参

  1. 参数的作用:可以在相同的url,但是指定不同的参数,来加载不同的数据

  2. 在flask中如何使用参数

    1
    2
    3
    @app.route("/article/<id>")
    def article(id):
    return "您请求的参数是:%s" %id
    • 参数需要放在两个尖括号中
    • 视图函数中需要放和url中的参数同名的参数

反转URL

正转

通过url可以取到视图函数的内容

反转

  1. 从视图函数到url的转换叫做反转url(通过视图函数的名称,可以得到指向的url
  2. 用处
    • 在页面重定向的时候,会使用url反转
    • 在模板中也会使用url反转

函数讲解

  • url_for():
    • params:视图函数名称, 视图函数对应的参数(如果视图函数有参数的话),如 url_for("article", id="123")
    • return: 视图函数对应的url

页面跳转和重定向

  1. 用处:在用户访问一些需要登录的页面的时候,如果用户没有登录,那么可以让他重定向到登录页面

  2. 实现

    1
    2
    3
    4
    5
    6
    7
    8
    from flask import Flask, redirect, url_for

    @app.route("/question/<isLogin>/")
    def question(isLogin):
    if isLogin == '1':
    return "这是发布问答页面"
    else:
    return redirect(url_for("login"))

函数讲解

  • redirect()
    • params: url,如 redirect("/login123/")或者 redirect(url_for("login")),建议使用第二种,只要视图函数名字不变, @app.route(/login123/)里面的 login123怎么变都没关系

模板渲染和参数

如何渲染模板

  1. 模板放在 templates 文件夹下

  2. flask 中导入 render_template 函数

  3. 在试图函数中,使用 render_template 函数渲染模板。

    注意:只需要填写模板的名字,不要填写 templates 这个文件夹的路径。如果在 templates 文件夹下创建了一个新的文件夹,那么就要添加上文件名(render_template() 函数的文件名是相对路径)

模板传参

  • 如果只有一个或者少量参数,直接在 render_template() 函数中添加关键字参数就可以了
  • 如果有多个参数的时候,那么可以先把所有的参数放在字典中,然后在 render_template 中,使用 **字典名 把字典转换为关键参数传递进去,这样的代码更方便管理和应用

函数讲解

  • render_template()
    • params: 渲染文件名(渲染文件放在template文件夹下), username = 用户名 (username 保证和渲染文件中的变量名保持一致), … 。如 render_template("index.html", username = "Qeuroal")
    • 如果参数特别多的话,可以先定义一个字典(context),然后把字典传入函数内,如 render_template("index.html", **contet)

模板访问属性和字典

在模板中如果要使用一个变量,语法是 {{ params }}

  • 访问类的属性:
    • 和在Python文件中一致
    • 或者类似于这样: Person["name"]
  • 访问字典:
    • py文件一样: dict[key]
    • dict.key

实现

template1.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@app.route("/")
def index():
class Person():
name = "Qeuroal"
age = 24

p = Person()
testDict = {
"key": "key",
"value": "value"
}
context = {
"username": "Qeuroal",
"gender": "男",
"age": 24,
"person": p,
"testDict": testDict
}

index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
这是HTML文件中出现的文字
<p>用户名: {{ username }}</p>
<hr>
<p>用户名: {{ person.name }}</p>
<p>年龄: {{ person["age"] }}</p>
<hr>
<p>key: {{ testDict.key }}</p>
<p>value: {{ testDict['value'] }}</p>
</body>
</html>

HTML

if判断

语法

1
2
3
4
5
6
7
{% if xxx and xxx %}
xxxx
{% elif xxx and xxx%}
xxxx
{% else %}
xxxx
{% endif %}

if的使用和python中相差无几。

for循环遍历列表和字典

字典遍历

字典的遍历,语法和 python 一样,可以使用 items(), keys(), values(), iteritems(), iterkeys(), itervalues()

1
2
3
{% for k,v in user.items() %}
<p>{{ k }}: {{ v }}</p>
{% endfor%}

列表遍历

语法和 python 一样

1
2
3
{% for website in websites %}
<p>{{ website }}</p>
{% endfor %}

过滤器

作用对象是模板中的变量:

介绍和语法

  • 介绍:过滤器可以处理变量,把原始的遍历经过处理后再展示出来。作用的对象是变量

  • 语法:

    1
    {{ avator|default("xxx")}}

default过滤器

如果当前变量不存在,这时候可以指定默认值。

length过滤器

求列表,字符串,字典,元组的长度。类似于:python 中的 len()

常用的过滤器

  • abs(value)
  • default(value, defaultValue, boolean=false)
  • escape(value)
  • first(value)
  • format(value, *args, **kwargs)
  • last(value)
  • length(value)
  • join(value, d="")
  • safe(value)

继承和block

继承

  • 作用:可以把一些公共的代码放在父模板中,避免每个模板写同样的代码

  • 语法:

    1
    {% extends "base.html" %}
  • 如果想在子模板中实现自己的内容:需要定义接口,类似于 java 的接口:html子模板需要实现自己内容,必须要放在父模板定义的接口里面,即block,不能在接口外面写代码

block

  • 作用:可以让子模板实现一些自己的需求。父模板需要提前定义好。
  • 注意点:
    • 子模板中的代码,必须放在block块中;
    • 可以定义多个 block

URL链接

使用 url_for(视图函数名称) 可以反转成url。

加载静态文件

1
2
3
4
5
6
<style>
a {
background: red;
color: black;
}
</style>

样式代码一般不会写在模板中,因为:维护性差,一般样式文件抽取成 css 文件(static 文件夹中是存放静态文件的)

语法

url_for("static", filename="路径")

  • 如:url_for("static", filename="css/index.css")

  • 静态文件,flask会从 static 文件夹中开始寻找,所以不需要写 static 这个路径了

  • 可以加载 css文件, js 文件,image 文件。

示例代码

1
2
3
4
5
6
{# 加载css文件 #}
<link rel="stylesheet" href="{{ url_for("static", filename="css/index.css") }}">
{# 加载js文件 #}
<script src="{{ url_for("static", filename="js/index.js") }}"></script>
{# 加载image文件 #}
<img src="{{ url_for("static", filename="images/pdx.jpg") }}" alt="">

注意点

装饰器

  • 装饰器为 @app.route('/login/'),网址可以是:http://127.0.0.1:5000/loginhttp://127.0.0.1:5000/login/
  • 装饰器为 @app.route('/login'),网址只能是:http://127.0.0.1:5000/login/

MySQL

安装(win10)

  • 我选择的是 Server only

  • 设置密码:

  • 打开命令行,并输入上一步设置好的密码

  • 下载地址

  • 一般直接下一步

  • 设置初始密码命令:

    1
    mysqladmin -uroot password [password]
  • 如果没有安装 .net Framework 4,就在那个提示框中,找到下载的 url 下载;

  • 如果没有安装 Microsoft visual C++ x64,那么就需要谷歌或者百度下载这个软件进行安装即可。

MySQL-python中间件介绍与安装

linux, macos

  1. 进入虚拟环境
  2. 输入 pip install mysql-python

windows系统

  • windows 在这里下载,看 python 安装的是32位还是64位,对应下载
  • 进入虚拟环境: conda activate 虚拟环境名
  • 安装:pip install mysqlclient-1.4.6-cp37-cp37m-win_amd64.whl

Flask-SQLAlchemy的介绍与安装

  • ORM: Object Relationship Mapping (关系模型映射);

  • flask-sqlalchemy 是一套 ORM框架

  • 模型关系映射

    • delete(article1):把 article表中和article1属性对应相同的那条数据删除。
    • 好处:ORM使得我们操作数据库非常简单,就像操作对象是一样的,非常方便。因为一个表就抽象成了一个类,一条数据就抽象成了该类的一个对象。
  • 安装:pip install flask-sqlalhemy,如果是在 macos 或者 linux 中没有权限的话这样安装: sudo pip install flask-sqlalhemy

Flask-SQLAlchemy的使用

  1. 初始化和设置数据库配置信息:

    • 使用 flask_sqlalchemy 中的 SQLAlchemy 进行初始化

      1
      2
      3
      from flask_sqlalchemy import SQLAlchemy
      app = Flask(__name__)
      db = SQLAlchemy(app)
  2. 设置配置信息:在 config.py 文件中添加以下配置信息

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    # dialect+driver://username:password@host:port/database
    DIALECT = "mysql"
    DRIVER = "mysqldb"
    USERNAME = "root"
    PASSWORD = "toor"
    HOST = "127.0.0.1"
    PORT = "3306"
    DATABASE = "dbDemo1"

    SQLALCHEMY_DATABASE_URI = "{}+{}://{}:{}@{}:{}/{}?charset=utf8".format(DIALECT, DRIVER, USERNAME, PASSWORD, HOST, PORT, DATABASE)
    # 去除警告
    SQLALCHEMY_TRACK_MODIFICATIONS = False
  3. 添加配置文件

    1
    2
    3
    import config

    app.config.from_object(config)
  4. 测试是否连接成功

    1
    db.create_all()

    如果没有报错,说明配置没有问题,如果有错误,可以根据错误进行修改。

建表

  1. cmd 进入 mysqlmysql -uroot -p
  2. create database dbDemo1 charset utf8

SQLAlchemy创建模型与表的映射

步骤

  1. 模型需要继承自 db.Model,然后需要映射到表中的属性,必须写成 db.Column()的数据类型

  2. 数据类型:

    • db.Integer: 整型
    • db.Sting: varcharchar,需要指定最长的长度
    • db.Text: text
  3. 其他参数

    • primary_key: 将这个字段设置为主键
    • autoincrement: 这个主键为自增长
    • nullable: 这个字段是否可以为空,默认为空,可以将这个值设置为 False ,在数据库中,这个值就不能为空了
  4. 最后需要调用 db.create_all() 来将模型真正的创建到数据库中,db.create_all()只会映射一次。

实例

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
from flask import Flask, render_template
from flask_sqlalchemy import SQLAlchemy
import config

app = Flask(__name__)
app.config.from_object(config)
db = SQLAlchemy(app)

"""
创建article表
create table article (
id int primary key autoincrement,
title varchar(100) not null,
content text not null
)
"""
# 自己写的模型一定要继承sqlalchemy属性下的model模型, 字段->属性
class Article(db.Model):
# 如果不指定表名,默认为类名
__tablename__ = "article"
# 属性id映射到字段id
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
# varchar, char -> db.String(长度)
title = db.Column(db.String(100), nullable=False)
content = db.Column(db.Text, nullable=False)

# 所有的模型映射到数据库的一张表
db.create_all()


@app.route('/')
def index():
return "index"

if __name__ == '__main__':
app.run()

SQLAlchemy数据增删改查

1
2
3
4
5
# 增加
article1 = Article(title="aaa", content="bbb")
db.session.add(article1)
# 事务
db.session.commit()

1
2
3
4
5
6
# 返回的是Query(<==> list); .all()查找所有数据, .first()返回的第一条数据,如果没有,返回null
result = Article.query.filter(Article.title == "aaa").all()
print(result[0], type(result[0]))
article1 = result[0]
print(article1.title)
print(article1.content)

1
2
3
4
5
6
# 1. 先把要修改的地方查找出来
article1 = Article.query.filter(Article.title == "aaa").first()
# 2. 把这条数据,你需要修改的地方进行修改
article1.title = "new title"
# 3. 做事务的提交
db.session.commit()

1
2
3
4
5
6
# 1. 把需要删除的数据查找出来
article1 = Article.query.filter(Article.content == "bbb").first()
# 2. 把这条数据删除掉
db.session.delete(article1)
# 3. 做事务提交
db.session.commit()

注意

  • 如果把增删改查放在 @app.route("/") 类似的视图函数中,访问一次 url,就会执行一遍相应的增删改查

Flask-SQLAlchemy外键及其关系

外键

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
"""
用户表
sql语句:
create table users (
id int primary key autoincrement,
username varchar(100) not null
)
"""
class User(db.Model):
__tablename__ = "user"
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
username = db.Column(db.String(100), nullable=False)
# password = db.Column(db.String(100), nullable=False)

"""
文章表
create table article (
id int primary key autoincrement,
title varchar(100) not null,
content text not null,
authorId int,
foreign key "authorId" references "user.id"
)
"""
class Article(db.Model):
__tablename__ = "article"
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
title = db.Column(db.String(100), nullable=False)
content = db.Column(db.Text, nullable=False)
# 设置外键
authorId = db.Column(db.Integer, db.ForeignKey("user.id"))
# params: 模型名, backref:反向引用;db.backref(), params: 反向引用的名字,
author = db.relationship("User", backref=db.backref("articles"))

解释

  • authorId = db.Column(db.Integer, db.ForeignKey("user.id"))
    • Article 这个模型添加一个 author 属性,可以访问这篇文章的作者的数据,像访问普通模型一样
    • backref:定义反向引用,可以通过 User.articles 这个模型访问这个模型缩写的所有文章

补充

  • 添加数据(方法二):

    1
    2
    3
    4
    article = Article(title="aaa", content="bbb")
    article.author = User.query.filter(User.id==1).first()
    db.session.add(article)
    db.session.commit()

多对多

例子

在这张图中:

使用

  • 多对多的关系,要通过一个中间表进行关联;

  • 中间表,不能通过 class 的方式实现,只能通过 db.Table 的方式实现

  • 设置关联:

    1
    tags = db.relationship("Tag", secondary=articleTagRelation, backref=db.backref("articles"))

    需要使用一个关键字参数 secondary=中间表 进行关联

  • 访问和添加可以通过以下方式操作:

    • 添加数据

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      article1 = Article(title="aaa")
      article2 = Article(title="bbb")
      tag1 = Tag(name="111")
      tag2 = Tag(name="222")

      article1.tags.append(tag1)
      article1.tags.append(tag2)

      article2.tags.append(tag1)
      article2.tags.append(tag2)

      db.session.add(article1)
      db.session.add(article2)
      db.session.add(tag1)
      db.session.add(tag2)
      db.session.commit()
    • 访问数据

      1
      2
      3
      4
      article1 = Article.query.filter(Article.title=="aaa").first()
      tags = article1.tags
      for tag in tags:
      print(tag.name)

Flask-Script的介绍和安装

介绍

Flask-Script 的作用是可以通过命令行的形式来操作 Flask。例如通过命令跑一个开发版本的服务器、设置数据库、定时任务。

安装

  1. 进入到虚拟环境中
  2. pip install flask-script进行安装

使用

  1. 如果直接在主 manage.py 中写命令,那么在终端就只需要 python manage.py commandName 就可以了。

  2. 如果把一些命令集中在一个文件中,那么在终端就需要输入一个父命令,如 python manage.py db init

  3. 例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    from flask_script import Manager
    from flaskScriptDemo import app
    from dbScript import dbManager

    manager = Manager(app)

    @manager.command
    def runserver():
    print("服务器跑起来了")

    # params: 前缀(类似于别名),dbManager
    # 使用:python manage.py db init
    manager.add_command("db", dbManager)

    if __name__ == '__main__':
    manager.run()
  4. 有子命令的例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    from flask_script import Manager

    dbManager = Manager()

    @dbManager.command
    def init():
    print("数据库初始化完成")

    @dbManager.command
    def migrate():
    print("数据表迁移成功")

分开 models 及解决循环引用

循环引用

解决办法:

db 放在一个单独的文件中,切断循环引用的线条就可以了。

分开 models

  • 目的:为了让代码更加方便的管理

注意:

  • 使用了 db.init_app(app),就不能直接使用 db.create_all()了,会报错。

    1
    2
    3
    app = Flask(__name__)
    app.config.from_object(modelSepConfig)
    db.init_app(app)

    因为涉及到了上下文的问题:

    解决办法看 flask-migrate

flask-migrate

db.init_app(app)不能直接使用 db.create_all() 解决办法

手动推动 appapp栈

介绍

因为采用 db.create_all() 在后期修改字段的时候,不会自动的映射到数据库中,必须删除表,然后重新运行 db.create_all() 才会重新映射,这样不符合我们的需求。因此 flask-migrate 就是为了解决这个问题,它可以在每次修改模型后,可将修改的东西映射到数据库中

安装

  1. 进入虚拟环境
  2. pip install flask-migrate

使用

使用 flask_migrate 必须借助 flask-scripts,这个包的 MigrateCommand 中包含了所有和数据库相关的命令。

相关命令

  • python manage.py db init: 初始化一个迁移脚本的环境,只需要执行一次;
  • python manage.py db migrate: 将模型生成迁移文件,只要模型更改了,就需要执行一遍这个命令;
  • python manage.py db upgrade: 将掐你文件真正映射到数据库中,每次运行了 migrate 命令后,就记得要运行这个命令

注意点

  • 如果需要将你想要映射到数据库中的模型,都要导入到 manage.py 文件中,如果没有导入进去,就不会映射到数据库中

manage.py 的相关代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from flask_script import Manager
from migrateDemo import app
from flask_migrate import Migrate, MigrateCommand
from exts import db
from models import Article

# 模型 => 迁移文件 => 表
# init: 初始化迁移的环境
# migrate: 将模型生成迁移文件
# upgrade: 将迁移文件映射成表

manager = Manager(app)

# 1. 要使用 flask_migrate,必须绑定app和db
migrate = Migrate(app, db)
# 2. 把MigrateCommand命令添加到manager中
manager.add_command("db", MigrateCommand)




if __name__ == '__main__':
manager.run()

cookie和session

  • 出现的原因:在网站中,http 请求是无状态的。也就是说即使第一次和服务器连接后并且登录成功后,第二次请求服务器依然不知道当前请求是哪个用户。cookie的出现就是我了解决这个问题,第一次登录后服务器返回一些数据(cookie)给浏览器,然后浏览器保存在本地,当该用户发送第二次请求的时候,就会自动的把上次请求存储的 cookie 数据自动的携带给服务器,服务器通过浏览器携带的数据就能判断当前用户是那个了;

  • 图解:

  • 如果服务器返回了 cookie 给浏览器,那么浏览器下次再请求相同的服务器的时候,就会自动的把 cookie 发送给服务器,这个过程,用户根本不需要管;

  • cookie 是保存在浏览器中的,相对对的是浏览器;

session

  • 介绍:sessioncookie 的作用有点类似,都是为了存储用户相关的信息。不同的是,cookie 是存储在本地服务器的,而 session 存储在服务器。存储在服务器的数据会更加安全,不容易被窃取。但存储在服务器也有一定的弊端,就是会占用服务器的资源,但现在服务器已经发展至今,一些 session 信息还是绰绰有余的。

  • 图解:

  • 使用 session 的好处:

    • 敏感数据不是直接发送给服务器,而是发送回一个 session_id ,服务器将 session_id 和敏感数据做一个映射存储在 session (服务器上面)中,更加安全;
    • session 可以设置过期时间,也可以从另外一方面,保证用户的账号安全。

注意

  • session_id:返回给浏览器的时候,是放在 cookie 中的。

flask中session工作机制

  • 讲解:把敏感数据经过加密后放入 session 中,然后再把 session 存放到 cookie 中,下次请求的时候,再从浏览器发送过来的 cookie 中读取 session,然后再从 session 中读取敏感数据,并进行解密,获取最终的用户数据;

  • flask 的这种 session 机制,可以节省服务器的开销,以内把所有的信息都存储到了客户端(浏览器);

  • 安全是相对的,没有绝对的安全,把 session 放到 cookie 中,经过机密,也是比较安全的;

  • 图解:

flask操作session

session的操作方式:

  • 使用 session 需要从 flask 中导入 session,以后所有和 session 相关的操作都是通过这个变量来的;
  • 使用 session 需要设置 SECRET_KEY,用来作为加密用的。并且这个 SECRET_KEY 如果每次服务器启动后都变化的话,那么之前的 session 就不能通过当前这个 SECRET_KEY 进行解密了;
  • 操作 session ,跟操作字典是一样的;
  • 添加 sessionsession[key]=value
  • 删除:session.pop(key) 或者 del session[key]
  • 清除所有 sessionsession.clear()
  • 获取 sessionsession.get(key)

设置session的过期时间

  • 如果没有指定session的过期时间,那么默认是浏览器关闭后就自动结束

  • 如果设置了 sessionpermanent 属性为 True,那么过期时间是31天

  • 可以通过给 app.config 设置 PERMANENT_SESSION_LIFETIME 来更改过期时间,这个值的数据类型是 datatime.timedelay 类型:

    1
    2
    3
    from datetime import timedelta

    PERMANENT_SESSION_LIFETIME = timedelta(days=7)

get请求和post请求

get请求

  • 使用场景:如果只对服务器获取数据,并没有对服务器产生任何影响,那么这时候使用 get 请求
  • 传参:get请求传参是防砸url中,并且是通过 ?的形式来指定 keyvalue

post请求

  • 使用场景:如果要对服务器产生影响,那么使用post请求,如: 登录:服务器要记录用户什么时候登陆过,要把数据保存在本地;
  • 传参:post请求传参不是放在 url 中,而是通过 form data 的形式发送给服务器的

获取参数

  • get请求:通过 flask.request.args 来获取
  • post请求:通过 flask.request.form 来获取

注意

  • post请求在模板中要注意几点:

    • input标签中,要写name来标识这个 valuekey,方便后台获取

    • 在写 form 表单的时候,要指定 method='post',并且要指定 action='/login/'

    • 示例代码

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      <form action="{{ url_for("login") }}" method="post">
      <table>
      <tbody>
      <tr>
      <td>用户名</td>
      <td><input type="text" placeholder="请输入用户名" name="username"></td>
      </tr>

      <tr>
      <td>密码</td>
      <td><input type="text" placeholder="请输入密码" name="password"></td>
      </tr>

      <tr>
      <td></td>
      <td><input type="submit" placeholder="登录"></td>
      </tr>

      </tbody>
      </table>
      </form>

保存全局遍历的g属性

g: global

  • g对象 是专门用来保存用户的数据的
  • g对象 在一次请求中的所有的代码的地方,都是可以使用的

钩子函数(hook)

钩子函数

正常执行情况:先执行A,然后再执行B。

钩子函数:可以在运行时,把C直接插入到AB之间

before_request

  • before_request:在请求之前执行的函数,且每次请求都会执行一遍
  • before_request是在视图函数执行之前执行的
  • before_request这个函数只是一个装饰器,他可以把需要设置为钩子函数的代码放到视图函数执行之前来执行

context_processor(上下文处理器)

  • 出现的原因:多个不同的页面需要相同的参数,如:username
  • 上下文处理器应该返回一个字典,字典中的 key 会被模板当成变量来渲染
  • 上下文处理器中返回的字典,在所有页面中都是可用的。
  • 被这个装饰器修饰的钩子函数,必须要返回一个字典,即使为空,也要返回

装饰器

  • 需求1:在打印run之前,先要打印hello world

    1
    2
    3
    4
    5
    def run():
    print("hello world")
    print("run")

    run()
  • 需求2:在所有函数执行之前,都要打印一个 hello world

    • 方法一:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      def run():
      print("hello world")
      print("run")

      def run1():
      print("hello world")
      print("run1")

      run()
    • 方法二(如果成千上万个函数,比较麻烦,且难以维护):装饰器

      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
      # 装饰器的两个特别之处:
      # 1. 参数是一个函数
      # 2. 返回值是一个函数
      def myLog(func):

      def wrapper():
      print("hello world")
      func()

      """
      补充:
      wrapper: 函数体
      wrapper(): 执行wrapper这个函数
      """
      return wrapper

      # 如何用
      @myLog
      def run():
      print("run")
      """
      等价于上面
      run = myLog(run) <==> wrapper
      run() <==> wrapper() <==> print("hello world"); func() <==> print("hello world"); print("run")
      理解:func() 执行的才是真的 run() 方法
      """
      run()

讲解

  • 装饰器的两个特别之处:
    • 参数是一个函数
    • 返回值是一个函数
  • 上面的 myLog() 是无参情况下的装饰器

进阶:有参情况下的装饰器

需求:在所有函数执行之前,都要打印一个 hello world

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 装饰器的两个特别之处:
# 1. 参数是一个函数
# 2. 返回值是一个函数
def myLog(func):

def wrapper(a, b):
print("hello world")
func(a, b)
return wrapper

@myLog
def add(a, b):
print("结果是", a + b)
"""
等价于上面
add = myLog(add) <==> wrapper
add(1, 2) <==> wrapper(1, 2) # 因此,如果wrapper()无参,就会报错 `TypeError: wrapper() takes 0 positional arguments but 2 were given`
"""
add(1, 2)

再进阶:有无参同时存在情况下的装饰器

  • 解决办法: *args, **kwargs:可以表示任何参数

    • *args: 位置参数,如:add(a, b) 中的ab,参数是一一对应的
    • **kwargs:关键字参数(key=value),如:add(a=abc)a就是传入的abc
    1
    2
    3
    4
    5
    6
    def myLog(func):

    def wrapper(*args, **kwargs):
    print("hello world")
    func(*args, **kwargs)
    return wrapper

    run(), add() 同上

再进阶

  • 问题:加完装饰器后,函数名被偷偷更改掉了,因此如何保留住原来的函数的名字(把名字改掉是很危险的行为)?

    • 没有装饰器的情况

      1
      2
      3
      def run():
      print("run")
      print(run.__name__) # run.__name__代表的是run这个函数的名称

      输出:

      run

    • 有装饰器的情况

      1
      2
      3
      4
      @myLog
      def run():
      print("run")
      print(run.__name__)

      输出:

      wrapper

  • 解决办法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    from functools import wraps

    def myLog(func):

    @wraps(func)
    def wrapper(*args, **kwargs):
    print("hello world")
    func(*args, **kwargs)
    return wrapper
    • 没加 @wraps 的情况
      run() <=> myLog(run)() <=> wrapper()

    • 加了 @wraps(func) 的情况

      • run <=> myLog(run) <=> @wraps(func)装饰的wrapper <=> wraps(wrapper) <=> wrapsFunction(这个函数是透明的,即看不到的,但是不管怎么样,返回的函数的__name__是run) => wrapsFunction.__name__ == "run"
      • ``run() <=> myLog(run)() <=> @wraps装饰的wrapper() <=> wraps(wrapper)() <=> wrapsFunction()`

再进阶

  • 需求:如果run()有返回值,该怎么办?

  • 解决办法:在 wrapper()return func(*args, **kwargs)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    from functools import wraps

    def myLog(func):

    @wraps(func)
    def wrapper(*args, **kwargs):
    print("hello world")
    return func(*args, **kwargs)
    return wrapper

    @myLog
    def run():
    return 1+1
  • 讲解:

    1
    2
    3
    4
    5
    run = myLog(run) = wrapper
    ==>(推出,下同) run = wrapper
    ==> run() = wrapper() = print("hello world"); return run() = print("hello world"); return 1+1
    因为run()需要返回1+1,所以wrapper()也需要返回1+1
    如果wrapper没有return,那么func(*args, **kwargs),就只是执行了一下1+1,别的什么都没有了

小结

  • 装饰器的使用是通过 @ 符号,放在函数上面;
  • 装饰器中定义的函数,要使用 *args**kwargs 两对兄弟的组合,并且在这个函数中执行原始函数的时候也要把 *args**kwargs 传进去;
  • 需要使用 functools.wraps 在装饰器中的函数上把传进来的这个函数进行一个包裹,这样就不会丢失原来的函数 __name__ 等属性

实践

概述

地球村:现代科技缩小世界的时空距离

信件 ——> 网络编程

计算机网络

计算机网络是指将地理位置不同的具有独立功能的多台计算机机器外部设备,通过通信线路连接起来,在网络操作系统,网络管理软件及光网络通信协议(类似于方言、语言)的管理和协调下,实现资源共享和信息传递的计算机系统。

网络编程的目的

无线电台… :传播交流信息,数据交换。(通信

想要达到这个效果需要什么

  1. 如何准确的定位网络上的一台主机 :192.168.16.124:port,定位到这个计算机上的某个资源
  2. 找到了这个主机,如何传输数据呢?

javaweb: 网页编程 B/S架构

网络编程: TCP/IP

网络通信的要素

如何实现网络的通信?

通信双方的地址

  • ip (唯一(指的是公网,不是局域网))
  • port
  • 192.168.16.124:5900ip:port):就可以定位到某台计算机上的某一个应用

规则:网络通信的协议

http, ftp, smtp, tcp, udp, ….

TCP/IP参考模型:

本章目的:

小结

  • 网络编程中有两个主要的问题
    • 如何准确的定位网络上的一台或多台主机
    • 找到主机之后如何进行通信
  • 网络编程中的要素
    • IPportip
    • 网络通信协议:udp, tcp
  • 万物皆对象

IP

ip地址:InetAddress

用处

  • 唯一定位一台网络上计算机
  • 127.0.0.1(localhost): 本机
  • ip地址的分类
    • IP地址分类:IPV4/IPV6
      • IPV4: 如127.0.0.1。4个字节(32位)组成(0-255),全球42亿个(30亿都在北美,亚洲4亿,2011年就用尽了)
      • IPV6: 如 2001:0bb2:aaaa:0015:0000:00000:1aaa:1312。16字节(128位)组成,8个 无符号整数(4个字节),用的是16进制(16进制占4位)。
    • 公网(互联网使用)和私网(局域网使用)
      • 192.168.xx.xx:局域网,专门给组织内部使用
      • ABCD类地址
    • 域名:记忆 IP 问题
      • IP: www.vip.com

端口

端口表示计算机上的一个程序的进程(类似于:一栋楼代表一个 ip,门牌号代表 端口)

  • 不同的进程有不同的端口号,用来区分软件

  • 被规定:0~65535

  • TCP/UDP端口: 65535 * 2,单个协议下端口号不能冲突,不同协议下,端口可以冲突

  • 端口分类

    • 公有端口:(0~1023)最好不要用

      • HTTP: 80
      • HTTPS:43
      • FTP:21
      • SSH:22
      • Telent: 23
    • 程序注册端口:1024~49151,分配给用户或者程序,建议不要用

      • Tomcat: 8080
      • Mysql:3306
      • Oracle: 1521
    • 动态、私有:49152~65535,建议不要用

      • Idea:63342

      • 查看所有接口

        1
        2
        3
        netstat -ano # 查看所有端口
        netstat -ano|findstr "5900"# 管道流: |,查看指定的端口
        tasklist|findstr "8696" # 查看指定端口的进程

        打开任务管理器: ctrl+shift+esc

    • 代码

      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
      package com.kuangstudy.net.module4;

      import java.net.InetSocketAddress;

      /**
      * @author Qeuroal
      * @date 2021-03-21 16:15
      * @description
      * @since
      */
      public class TestInetSocketAddress {
      public static void main(String[] args) {

      InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1", 8080);
      System.out.println(inetSocketAddress);
      InetSocketAddress inetSocketAddress2 = new InetSocketAddress("localhost", 8080);
      System.out.println(inetSocketAddress2);

      System.out.println(inetSocketAddress.getAddress());
      // 地址,可以更改hosts文件来更改映射
      System.out.println(inetSocketAddress.getHostName());
      // 端口
      System.out.println(inetSocketAddress.getPort());
      }
      }
    • 图片

通信协议

  • 协议: 约定,就好比我们现在说的普通话
  • 网络通信协议:针对于网络所产生的协议,如:速率,传输码率,代码结构,传输控制……
  • 问题:非常的复杂
  • 大事化小:分层
  • TCP/IP协议簇:实际上是一组协议,不止两个协议。

重要的协议:

  • TCP: 用户传输协议,类似于打电话
  • UDP: 用户数据报协议,类似于发短信

出名的协议:

  • TCP: 用户传输协议
  • IP:网络互连协议

TCP, UDP对比

  • TCP: 打电话

    • 连接:稳定

    • 连接:三次握手四次挥手

      • 三次握手:

        最少需要三次,保证稳定连接!

        A:你瞅啥?

        B:瞅你咋地?

        A:干一场!

      • 四次挥手

        A:我要走了!

        B:你真的要走了吗!

        B:你真的真的要走了吗?

        A:我真的要走了!

    • 客户端、服务端连接

    • 传输完成,释放连接,效率低

  • UDP: 发短信

    • 不连接:不稳定
    • 客户端、服务端连接:没有明确的界限
    • 不管有没有准备好,都可以发给你
    • 类似于导弹攻击
    • DDOS: 洪水攻击(饱和攻击)

TCP

客户端

  1. 连接服务器 socket
  2. 发送消息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
package com.kuangstudy.net.module6;

import java.io.IOException;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.Socket;

/**
* @author Qeuroal
* @date 2021-03-21 16:53
* @description 客户端
* @since
*/
public class TestTcpClient {
public static void main(String[] args) {
Socket socket = null;
OutputStream os = null;
try {
// 1. 要知道服务器的地址
InetAddress serverIP = InetAddress.getByName("127.0.0.1");
// 2. 端口号
int port = 9999;
// 3. 创建一个socket连接
socket = new Socket(serverIP, port);
// 4. 发送消息: io流
os = socket.getOutputStream();
os.write("你好,欢迎学习网络编程".getBytes());
} catch (Exception e) {
e.printStackTrace();
} finally {
if (os != null) {
try {
os.close();
} catch (IOException e) {
e.printStackTrace();
}
}

if (socket != null) {
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}

服务器

  1. 建立服务的端口 ServerSocket
  2. 等待用户的连接 accept
  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
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
package com.kuangstudy.net.module6;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;

/**
* @author Qeuroal
* @date 2021-03-21 16:53
* @description 服务端
* @since
*/
public class TestTcpSever {
public static void main(String[] args) {
ServerSocket serverSocket = null;
Socket socket = null;
InputStream is = null;
ByteArrayOutputStream baos = null;
try {
// 1. 我得有一个地址
serverSocket = new ServerSocket(9999);
while (true) {
// 2. 等待客户端连接过来
socket = serverSocket.accept();
// 3. 读取客户端的消息
is = socket.getInputStream();

// 管道流
baos = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int len;
while ((len = is.read(buffer)) != -1) {
baos.write(buffer, 0, len);
}
System.out.println(baos.toString());
}

} catch (IOException e) {
e.printStackTrace();
} finally {
// 关闭资源
if (baos != null) {
try {
baos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (is != null) {
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (socket != null) {
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}

if (serverSocket != null) {
try {
serverSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}

文件上传

读取文件->流->传出去

服务器端

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.kuangstudy.net.module7;

import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;

/**
* @author Qeuroal
* @date 2021-03-22 16:13
* @description
* @since
*/
public class TestTcpFileServer {
public static void main(String[] args) throws Exception {
// 1. 创建服务
ServerSocket serverSocket = new ServerSocket(9000);
// 2. 监听客户端的连接
Socket socket = serverSocket.accept();// 阻塞式监听,会一直等待客户端连接
// 3. 获取输入流
InputStream is = socket.getInputStream();
// 4. 文件输出
FileOutputStream fos = new FileOutputStream(new File("receive.png"));
byte[] buffer = new byte[1024];
int len;
while ((len = is.read(buffer)) != -1) {
fos.write(buffer, 0, len);

}
// 5. 通知客户端接收完毕
OutputStream os = socket.getOutputStream();
os.write("我接收完毕了,你可以断开了".getBytes());
// 6. 关闭资源
fos.close();
is.close();
socket.close();
serverSocket.close();
}
}

客户端

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
package com.kuangstudy.net.module7;

import java.io.*;
import java.net.InetAddress;
import java.net.Socket;

/**
* @author Qeuroal
* @date 2021-03-22 16:07
* @description
* @since
*/
public class TestTcpFileClient {
public static void main(String[] args) throws Exception {
// 1. 创建一个socket连接
Socket socket = new Socket(InetAddress.getByName("127.0.0.1"), 9000);
// 2. 创建一个输出流
OutputStream os = socket.getOutputStream();
// 3. 读取文件
FileInputStream fis = new FileInputStream(new File("src/resource/xly2.png"));
// 4. 写出文件
byte[] buffer = new byte[1024];
int len;
while ((len = fis.read(buffer)) != -1) {
os.write(buffer, 0, len);
}
// 5. 通知服务器,我已经结束了
socket.shutdownOutput(); // 我已经传输完了
// 5. 确定服务器接收完毕,才能够断开连接
InputStream is = socket.getInputStream();
// String byte[]
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buffer2 = new byte[1024];
int len2;
while ((len2 = is.read(buffer2)) != -1) {
baos.write(buffer2, 0, len2);
}
System.out.println(baos.toString());

// 5. 关闭资源
baos.close();
is.close();
fis.close();
os.close();
socket.close();
}
}

Tomcat

服务端

  • 自定义 S
  • Tomcat服务器 S: Java后台开发!

客户端

  • 自定义 S
  • 浏览器 B

UDP

发短信:不用连接,需要知道对方的地址


涉及到两个类:

  • DatagramPacket
  • DatagramSocket

发送端

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
package com.kuangstudy.net.module9;

import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;

/**
* @author Qeuroal
* @date 2021-03-22 17:29
* @description 不需要连接服务器
* @since
*/
public class TestUdpClient {
public static void main(String[] args) throws Exception {
// 1. 建立一个Socket
DatagramSocket socket = new DatagramSocket(); // 用来发东西的
// 2. 建个包
String msg = "你好啊,服务器!";
// 3. 发送给谁
InetAddress localhost = InetAddress.getByName("localhost");
int port = 9090;
// 数据,数据的长度起始,要发送给谁
DatagramPacket packet = new DatagramPacket(msg.getBytes(), 0, msg.getBytes().length, localhost, port);
// 4. 发送包
socket.send(packet);

// 5. 关闭
socket.close();
}
}

接收端

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

import java.net.DatagramPacket;
import java.net.DatagramSocket;

/**
* @author Qeuroal
* @date 2021-03-22 17:35
* @description 还是要等待客户端的连接
* @since
*/
public class TestUdpServer {
public static void main(String[] args) throws Exception {
// 开放端口
DatagramSocket socket = new DatagramSocket(9090);
// 接收数据包
byte[] buffer = new byte[1024];
DatagramPacket packet = new DatagramPacket(buffer, 0, buffer.length); // 接收
socket.receive(packet); // 阻塞接收
System.out.println(packet.getAddress().getHostName());
System.out.println(new String(packet.getData(), 0, packet.getLength()));

// 关闭连接
socket.close();

}
}

本质上没有服务器:因为可以互相发送,因此就没有服务器的概念

咨询

类似于:广告的客服

xxx: 你好

xxx: 你好

  • BufferedReader : 包装流包装 System.in,为了控制台读取

    1
    new BufferedReader(new InputStreamReader(System.in))

循环发送消息

1

循环接收消息

1

在线咨询

两个人都是发送方,同时也都是接收方

TalkSend

1

TalkReceive

1

TalkStudent

1

TalkTeacher

1

URL

如:https://www.baidu.com/

统一资源定位符:定位资源的,定位互联网上的某一个资源

DNS域名解析: 将 www.baidu.com ==> xxx.xx.xx.xx

组成(可以少,但不能多)

1
协议://ip地址:端口号/项目名/资源
  • URL() : 网络类,代表一个地址
    • param: String
    • url.getProtocol: 得到协议名
    • url.getHost(): 得到主机ip
    • url.getPort(): 得到端口
    • url.getPath(): 文件地址
    • url.getFile(): 得到文件全路径
    • url.getQuery: 得到参数(如:查询的名字)
    • url.openConnection(): 打开连接
    • urlConnection.getInputStream(): 得到流
    • urlConnection.disconnect(): 断开连接

下载文件

  1. 下载地址
  2. 连接到这个资源,用 HTTP 连接
  3. 下载

getResource

getResource读取的是 out 下的文件,即 classpath

  • 相对路径: 即在当前包内的路径,如: Test.class.getResource("xly2.png");, xly2.png在当前包内,或者说和运行的class在同一目录下
  • 绝对路径: 用 / 表示,代表是当前项目下,如 Test.class.getResource("/resource/xly2.png"),如上图可见 resource 的位置

new File

读取的是 目录文件,如下所示

1
new FileInputStream(new File("src/resource/xly2.png"));

GUI简介

简介

GUI核心开发技术:Swing、AWT
不流行原因:

  • 界面不美观
  • 需要 jre 环境

为什么要学习?

  1. 可以写出自己心中想要的小工具
  2. 工作时候,也可能小维护到 swing 界面,概率极小
  3. 了解MVC架构,了解监听

AWT

AWT介绍

  1. 包含了很多类和接口用于GUI编程!GUI:图形用户界面
  2. 元素:窗口,按钮,文本框
  3. java.awt

组件和容器

Frame

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
import java.awt.*;

/**
* @author QeuroIzo
* @date 2021-03-03 17:04
* @TODO GUI的第一个界面
* @since
*/
public class TestFrame {
public static void main(String[] args) {
// Frame 看源码
Frame frame = new Frame("我的第一个Java图像界面窗口");
// 设置可见性
frame.setVisible(true);
// 设置大小
frame.setSize(400, 400);
frame.setBackground(new Color(0, 255, 0));
// 设置弹出的初始位置
frame.setLocation(200, 200);
// 设置大小固定
frame.setResizable(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
package com.kuangstudy.gui.module3;

import java.awt.*;

/**
* @author QeuroIzo
* @date 2021-03-03 17:24
* @TODO
* @since
*/
public class TestFrame2 {
public static void main(String[] args) {
MyFrame myFrame1 = new MyFrame(100, 100, 200, 200, Color.BLUE);
MyFrame myFrame2 = new MyFrame(300, 100, 200, 200, Color.YELLOW);
MyFrame myFrame3 = new MyFrame(100, 300, 200, 200, Color.RED);
MyFrame myFrame4 = new MyFrame(300, 300, 200, 200, Color.MAGENTA);

}
}

class MyFrame extends Frame {
// 可能存在多个窗口,需要一个计数器
static int id = 0;
public MyFrame(int x, int y, int w, int h, Color color) {
super("MyFrame" + (++id));
setVisible(true);
setBounds(x, y, w, h);
setBackground(color);

}
}

面板Panel

Panel 可以看成是一个空间,但是不能单独存在,需要放在 Frame

解决了窗口关闭事件

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
package com.kuangstudy.gui.module4;

import java.awt.*;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;

/**
* @author QeuroIzo
* @date 2021-03-03 18:38
* @TODO
* @since
*/
public class TestPanel1 {
public static void main(String[] args) {
Frame frame = new Frame();
frame.setTitle("hello");
// 布局的概念
Panel panel = new Panel();

// 设置布局
frame.setLayout(null);
// 坐标
frame.setBounds(300, 300, 500, 500);
frame.setBackground(new Color(0, 255, 0));
// panel 设置坐标,相对位置
panel.setBounds(50, 50, 400, 400);
panel.setBackground(new Color(255, 0, 0));

// frame 添加 panel
frame.add(panel);
// 设置可见
frame.setVisible(true);

// 监听事件:监听窗口关闭事件 System.exit(0)
// 适配器模式:
frame.addWindowListener(new WindowAdapter() {
// 窗口点击关闭的时候需要做的事情
@Override
public void windowClosing(WindowEvent e) {
// 结束程序
System.exit(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
package com.kuangstudy.gui.module5;

import java.awt.*;

/**
* @author QeuroIzo
* @date 2021-03-03 19:01
* @TODO 流式布局
* @since
*/
public class TestFlowLayout1 {
public static void main(String[] args) {
Frame frame = new Frame();

// 组件-按钮
Button button1 = new Button("Button1");
Button button2 = new Button("Button2");
Button button3 = new Button("Button3");

// 设置为流式布局
// frame.setLayout(new FlowLayout()); // 默认center
// frame.setLayout(new FlowLayout(FlowLayout.LEFT));
frame.setLayout(new FlowLayout(FlowLayout.RIGHT));
frame.setSize(200, 200);
// 把按钮添加上去
frame.add(button1);
frame.add(button2);
frame.add(button3);
frame.setVisible(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
package com.kuangstudy.gui.module5;

import java.awt.*;

/**
* @author QeuroIzo
* @date 2021-03-03 21:02
* @TODO 东西南北中
* @since
*/
public class TestBorderLayout {
public static void main(String[] args) {
Frame frame = new Frame("Test");

Button east = new Button("East");
Button west = new Button("West");
Button south = new Button("South");
Button north = new Button("North");
Button center = new Button("Center");

frame.add(east, BorderLayout.EAST);
frame.add(west, BorderLayout.WEST);
frame.add(south, BorderLayout.SOUTH);
frame.add(north, BorderLayout.NORTH);
frame.add(center, BorderLayout.CENTER);

frame.setSize(200, 200);
frame.setVisible(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
package com.kuangstudy.gui.module5;

import java.awt.*;

/**
* @author QeuroIzo
* @date 2021-03-03 21:20
* @TODO
* @since
*/
public class TestGridLayout {
public static void main(String[] args) {
Frame frame = new Frame("GridLayout");

Button btn1 = new Button("btn1");
Button btn2 = new Button("btn2");
Button btn3 = new Button("btn3");
Button btn4 = new Button("btn4");
Button btn5 = new Button("btn5");
Button btn6 = new Button("btn6");

frame.setLayout(new GridLayout(3, 2));
frame.add(btn1);
frame.add(btn2);
frame.add(btn3);
frame.add(btn4);
frame.add(btn5);
frame.add(btn6);

// java函数:自动布局
frame.pack();
frame.setSize(300, 300);
frame.setVisible(true);

}
}

练习

切记直接动手

正常的方式:构思(80%) -> 代码(20%)

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.kuangstudy.gui.module6;

import java.awt.*;

/**
* @author QeuroIzo
* @date 2021-03-03 21:32
* @TODO
* @since
*/
public class TestDemo {
public static void main(String[] args) {
Frame frame = new Frame("表格布局测试");
frame.setLayout(new GridLayout(2, 3));

Panel panelUp = new Panel();
panelUp.setLayout(new GridLayout(2, 1));
Panel panelDown = new Panel(new GridLayout(2, 2));
Button btn1 = new Button("btn");
Button btn2 = new Button("btn");
Button btn3 = new Button("btn");
Button btn4 = new Button("btn");
Button btn5 = new Button("btn");
Button btn6 = new Button("btn");
Button btn7 = new Button("btn");
Button btn8 = new Button("btn");
Button btn9 = new Button("btn");
Button btn10 = new Button("btn");

// panelUp 添加 btn
panelUp.add(btn2);
panelUp.add(btn3);
// panelDown 添加 Btn
panelDown.add(btn6);
panelDown.add(btn7);
panelDown.add(btn8);
panelDown.add(btn9);

frame.add(btn1);
frame.add(panelUp);
frame.add(btn4);
frame.add(btn5);
frame.add(panelDown);
frame.add(btn10);
frame.pack();

frame.setBounds(300, 300, 500, 400);
frame.setBackground(Color.BLUE);
frame.setVisible(true);
}
}

总结

  1. Frame 是一个顶级窗口
  2. Panel 无法单独显示,必须添加到某个容器中
  3. 布局管理器
    1. 流式
    2. 东西南北中
    3. 表格
  4. 大小、定位、背景颜色、可见性、监听

事件监听

事件监听:当某个事情发生的时候,干什么?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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
package com.kuangstudy.gui.module7;

import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;

/**
* @author QeuroIzo
* @date 2021-03-05 15:39
* @TODO
* @since
*/
public class TestActionEvent {
public static void main(String[] args) {
// 按下按钮是,触发一些事件
Frame frame = new Frame();
// frame.
Button button = new Button("button");
// 因为 addActionListener() 需要一个 ActionListener,所以我们需要构造一个ActionListener
MyActionListener myActionListener = new MyActionListener();
button.addActionListener(myActionListener);

frame.add(button, BorderLayout.CENTER);
frame.pack();
frame.setVisible(true);
windowClose(frame);

}

/**
* 关闭窗体的事件
* @param frame Frame
*/
private static void windowClose(Frame frame) {
frame.addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
System.exit(0);
}
});
}
}

/**
* 事件监听
*/
class MyActionListener implements ActionListener {

@Override
public void actionPerformed(ActionEvent e) {
System.out.println("aaa");
}
}

多个按钮共享一个事件

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
package com.kuangstudy.gui.module7;

import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

/**
* @author QeuroIzo
* @date 2021-03-05 16:20
* @TODO
* @since
*/
public class TestActionEvent2 {
public static void main(String[] args) {
// 两个按钮,实现同一个监听
// 开始-停止
Frame frame = new Frame("开始-停止");
Button beginButton = new Button("start");
Button stopButton = new Button("stop");

// 可以显示的定义触发会返回的命令,如果不显示定义,则会走默认的只
// 可以多个按钮只写一个监听类
stopButton.setActionCommand("button-stop");
MyMonitor myMonitor = new MyMonitor();
beginButton.addActionListener(myMonitor);
stopButton.addActionListener(myMonitor);

frame.add(beginButton, BorderLayout.NORTH);
frame.add(stopButton, BorderLayout.SOUTH);

frame.pack();
frame.setVisible(true);


}
}

class MyMonitor implements ActionListener {

@Override
public void actionPerformed(ActionEvent e) {
// e.getActionCommand() 获得按钮的信息
System.out.println("按钮被点击了:msg=>" + e.getActionCommand());

}
}

输入框 TextField、监听

main方法里面只有一行代码:启动

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
package com.kuangstudy.gui.module8;

import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

/**
* @author QeuroIzo
* @date 2021-03-05 16:32
* @TODO
* @since
*/
public class TestTextField {
public static void main(String[] args) {
// 启动
new MyFrame();
}
}

class MyFrame extends Frame {
public MyFrame() {
TextField textField = new TextField();
add(textField);
// 监听这个文本框输入的文字
MyActionListener myActionListener = new MyActionListener();
// 按下enter,就会触发这个输入框的事件
textField.addActionListener(myActionListener);
// 设置替换编码
textField.setEchoChar('*');

setVisible(true);
pack();
}
}

class MyActionListener implements ActionListener {

@Override
public void actionPerformed(ActionEvent e) {
// 获得一些资源,返回的一个对象(为什么Object可以向下转型,有的时候不是会报错吗- runtime error! ClassCastException?)
TextField field = (TextField) e.getSource();
// 获得输入框中的文本
System.out.println(field.getText());
// 设置清空
field.setText("");
}
}

简单计算器、组合+内部类回顾

oop原则:组合大于继承

继承

1
2
3
class A extends B {

}

组合

1
2
3
class A {
public B 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
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
package com.kuangstudy.gui.module9;

import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

/**
* @author QeuroIzo
* @date 2021-03-05 18:06
* @TODO 简易计算器
* @since
*/
public class CalculateDemo {
public static void main(String[] args) {
new Calculator();
}
}

/**
* 计算器类
*/
class Calculator extends Frame {
public Calculator() {
// 三个文本框
TextField num1 = new TextField(10);
TextField num2 = new TextField(10);
TextField num3 = new TextField(20);
// 一个按钮
Button button = new Button("=");
button.addActionListener(new MyCalculatorListener(num1, num2, num3));
// 一个标签
Label label = new Label("+");

// 布局
setLayout(new FlowLayout());

// 添加组件
add(num1);
add(label);
add(num2);
add(button);
add(num3);

pack();
setVisible(true);
}
}

/**
* 监听器类
*/
class MyCalculatorListener implements ActionListener {

/**
* 获取三个变量
*/
private TextField num1, num2, num3;
public MyCalculatorListener(TextField num1, TextField num2, TextField num3) {
this.num1 = num1;
this.num2 = num2;
this.num3 = num3;
}

@Override
public void actionPerformed(ActionEvent e) {
// 1. 获得加数和被加数
int n1 = Integer.parseInt(num1.getText());
int n2 = Integer.parseInt(num2.getText());

// 2. 将这个值加法运算后,放到第三个框
num3.setText("" + (n1 + n2));
// 3. 清楚前两个框
num1.setText("");
num2.setText("");
}
}

优化代码

在一个类中调用另一个类的引用

多用组合,最好不要用继承、多态:继承,增强了代码的耦合性;多态,使代码更麻烦,理解容易出错。使用组合的方式把代码拿过来。

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
package com.kuangstudy.gui.module9;

import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

/**
* @author QeuroIzo
* @date 2021-03-05 18:06
* @TODO 简易计算器
* @since
*/
public class CalculateDemo {
public static void main(String[] args) {
new Calculator().loadFrame();
}
}

/**
* 计算器类
*/
class Calculator extends Frame {

/**
* 属性
*/
public TextField num1, num2, num3;

/**
* 方法
*/
public void loadFrame() {
// 三个文本框
num1 = new TextField(10);
num2 = new TextField(10);
num3 = new TextField(20);
// 一个按钮
Button button = new Button("=");
// 一个标签
Label label = new Label("+");
button.addActionListener(new MyCalculatorListener(this));

// 布局
setLayout(new FlowLayout());

// 添加组件
add(num1);
add(label);
add(num2);
add(button);
add(num3);

pack();
setVisible(true);
}

}

/**
* 监听器类
*/
class MyCalculatorListener implements ActionListener {

/**
* 获取计算器这个对象,在一个类中组合另外一个类
*/
private Calculator calculator = null;
public MyCalculatorListener(Calculator calculator) {
this.calculator = calculator;
}

@Override
public void actionPerformed(ActionEvent e) {
// 1. 获得加数和被加数
// 2. 将这个值加法运算后,放到第三个框
// 3. 清楚前两个框

int n1 = Integer.parseInt(calculator.num1.getText());
int n2 = Integer.parseInt(calculator.num2.getText());
calculator.num3.setText("" + (n1 + n2));
calculator.num1.setText("");
calculator.num2.setText("");
}
}

完全改造为OOP写法

内部类

  • 更好的包装
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
package com.kuangstudy.gui.module9;

import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

/**
* @author QeuroIzo
* @date 2021-03-05 18:06
* @TODO 简易计算器
* @since
*/
public class CalculateDemo {
public static void main(String[] args) {
new Calculator().loadFrame();
}
}

/**
* 计算器类
*/
class Calculator extends Frame {

/**
* 属性
*/
private TextField num1, num2, num3;

/**
* 方法
*/
public void loadFrame() {
// 三个文本框
num1 = new TextField(10);
num2 = new TextField(10);
num3 = new TextField(20);
// 一个按钮
Button button = new Button("=");
// 一个标签
Label label = new Label("+");
button.addActionListener(new MyCalculatorListener());

// 布局
setLayout(new FlowLayout());

// 添加组件
add(num1);
add(label);
add(num2);
add(button);
add(num3);

pack();
setVisible(true);
}

/**
* 监听器(内部类)
* 内部类最大的好处,就是可以畅通无阻的访问外部类的属性和方法
*/
private class MyCalculatorListener implements ActionListener {

@Override
public void actionPerformed(ActionEvent e) {
// 1. 获得加数和被加数
// 2. 将这个值加法运算后,放到第三个框
// 3. 清楚前两个框

int n1 = Integer.parseInt(num1.getText());
int n2 = Integer.parseInt(num2.getText());
num3.setText("" + (n1 + n2));
num1.setText("");
num2.setText("");
}
}
}

画笔

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.kuangstudy.gui.module10;

import java.awt.*;

/**
* @author QeuroIzo
* @date 2021-03-05 20:18
* @TODO
* @since
*/
public class TestPaint {
public static void main(String[] args) {
new MyPaint().loadFrame();
}
}

class MyPaint extends Frame {

public void loadFrame() {
setBounds(200, 200, 600, 500);
setVisible(true);
}
/**
* 画笔
* @param g
*/
@Override
public void paint(Graphics g) {
// super.paint(g);
// 画笔,需要有颜色,可以画画
g.setColor(Color.RED);
g.drawOval(100, 100, 100, 200);
// 实心圆
g.fillOval(100, 300, 100, 100);

g.setColor(Color.GREEN);
g.fillRect(200, 100, 100, 100);

// 养成习惯:画笔用完,将它还原到最初的颜色。
}
}

鼠标监听

目的:想要实现鼠标画画

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
package com.kuangstudy.gui.module11;

import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import java.util.Iterator;

/**
* @author QeuroIzo
* @date 2021-03-05 21:54
* @TODO 测试鼠标监听事件
* @since
*/
public class TestMouseListener {
public static void main(String[] args) {
new MyFrame("画图");
}
}

class MyFrame extends Frame {
// 画画需要画笔,需要监听鼠标当前的位置,需要集合来存储这个点
/**
* 存鼠标点击的点
*/
private ArrayList<Point> points;
public MyFrame(String title) {
super(title);
setBounds(200, 200, 600, 500);
// 存鼠标的点
points = new ArrayList<>();
// points.add(new Point(100, 100));
points.add(new Point(0, 0));
// points.add(new Point(200, 200));
// points.add(new Point(300, 300));


// 鼠标监听器,针对这个窗口
this.addMouseListener(new MyMouseListener());

setVisible(true);
}

@Override
public void paint(Graphics g) {
// 画画需要监听鼠标的事件
Iterator iterator = points.iterator();
while (iterator.hasNext()) {
Point point = (Point) iterator.next();
g.setColor(Color.BLUE);
g.fillOval(point.x, point.y, 100, 100);
}
}

/**
* 添加一个点到界面上
*/
public void addPaint(Point point) {
points.add(point);
}

/**
* 适配器模式
*/
private class MyMouseListener extends MouseAdapter {
// 只需要鼠标按下、弹起、按住不放

@Override
public void mouseClicked(MouseEvent e) {
MyFrame myFrame = (MyFrame) e.getSource();
// 点击的时候,就会在界面上产生一个点!
// 这个点就是鼠标的点
myFrame.addPaint(new Point(e.getX(), e.getY()));

// 每次点击鼠标都需要重画一遍(《=》刷新),每秒刷新 30帧或60帧
myFrame.repaint();
}
}
}

代码的思维导图,下图:

窗口监听

关掉某个窗口:即隐藏这个窗口

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
package com.kuangstudy.gui.module12;

import java.awt.*;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;

/**
* @author Qeuroal
* @date 2021-03-09 16:28
* @description
* @since
*/
public class TestWindowListener {
public static void main(String[] args) {
new WindowFrame();
}
}

class WindowFrame extends Frame {
public WindowFrame() {
setVisible(true);
setBounds(200, 300, 300, 400);
setBackground(Color.RED);
// addWindowListener(new MyWindowListener());
//最好使用匿名内部类
this.addWindowListener(new WindowAdapter() {
// 监听不到
// @Override
// public void windowOpened(WindowEvent e) {
// System.out.println("windowOpened");
// }

/**
* 关闭窗口
* @param e
*/
@Override
public void windowClosing(WindowEvent e) {
System.out.println("windowClosing");
System.exit(0);
}

// 监听不到
// @Override
// public void windowClosed(WindowEvent e) {
// System.out.println("windowClosed");
// }

/**
* 激活窗口
* @param e
*/
@Override
public void windowActivated(WindowEvent e) {
// 获取事件所作用的对象,(获得事件监听的对象)即你所与该事件绑定的控件,例如你点击了按钮,那么得到的source就是按钮对象了
WindowFrame source = (WindowFrame) e.getSource();
source.setTitle("被激活了");
System.out.println("windowActivated");
}

/**
* 未被激活窗口,即切出去了
* @param e
*/
@Override
public void windowDeactivated(WindowEvent e) {
System.out.println("windowDeactivated");
}
});
}

/** 成员内部类
class MyWindowListener extends WindowAdapter {
@Override
public void windowClosing(WindowEvent e) {
// 隐藏窗口,通过按钮隐藏窗口
setVisible(false);
// 正常退出:0,非正常退出:1
System.exit(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
package com.kuangstudy.gui.module13;

import java.awt.*;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;

/**
* @author Qeuroal
* @date 2021-03-09 17:04
* @description
* @since
*/
public class TestKeyListener {
public static void main(String[] args) {
new KeyFrame();
}
}

class KeyFrame extends Frame {
public KeyFrame() {
setBounds(300, 400, 300, 400);
setVisible(true);

this.addKeyListener(new KeyAdapter() {
/**
* 键盘按下
* @param e
*/
@Override
public void keyPressed(KeyEvent e) {
// 获得键盘下的键是哪个,当前键盘的码
int keyCode = e.getKeyCode();
// 不需要记录这个数值,直接使用静态属性 VK_xxx
System.out.println(keyCode);
if (keyCode == KeyEvent.VK_UP) {
System.out.println("你按下了上键");
}
// 根据按下的不同操作,产生不同结果
}
});
}
}

Swing

AWT 是底层,Swing 是给封装了

窗口、面板

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
package com.kuangstudy.gui.module14;

import javax.swing.*;
import java.awt.*;

/**
* @author Qeuroal
* @date 2021-03-09 17:17
* @description
* @since
*/
public class TestJFrame {
/**
* 初始化
*/
public void init() {
// 顶级窗口
JFrame jf = new JFrame("这是一个JFrame窗口");
jf.setVisible(true);
jf.setBounds(100, 100, 400, 300);

// 设置文字: Jlabel
JLabel label = new JLabel("欢迎来到JAVA GUI");

jf.add(label);

// 容器:需要实例化,JFrame本身也是一个容器,需要实例化
Container contentPane = jf.getContentPane();
contentPane.setBackground(Color.RED);

// 关闭事件
jf.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
}
public static void main(String[] args) {
// 建立一个窗口
new TestJFrame().init();
}
}

标签居中

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
package com.kuangstudy.gui.module14;

import javax.swing.*;
import java.awt.*;

/**
* @author Qeuroal
* @date 2021-03-09 17:32
* @description
* @since
*/
public class TestJFrame2 {
public static void main(String[] args) {
new MyJFrame2().init();
}
}

class MyJFrame2 extends JFrame {
public void init() {
this.setVisible(true);
setBounds(300, 300, 400, 300);
// 设置文字: Jlabel
JLabel label = new JLabel("欢迎来到JAVA GUI");
// add(label) 和 this.add(label) 一样
add(label);
//设置水平对齐
label.setHorizontalAlignment(SwingConstants.CENTER);
// 获得一个容器
Container contentPane = this.getContentPane();
contentPane.setBackground(Color.RED);
}
}

弹窗

JDialog,用来被弹出,默认就有关闭事件

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
package com.kuangstudy.gui.module15;

import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

/**
* @author Qeuroal
* @date 2021-03-09 20:46
* @description 主窗口
* @since
*/
public class TestDialog extends JFrame {
public TestDialog() {
this.setVisible(true);
this.setBounds(400, 400, 400, 300);
this.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);

// JFrame放东西:容器
Container contentPane = this.getContentPane();
// 绝对布局
contentPane.setLayout(null);

// 按钮
JButton jButton = new JButton("点击弹出一个对话框");
jButton.setBounds(30, 30, 200, 50);

// 点击这个按钮的时候,弹出一个弹窗
jButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
// 弹窗
new MyDialogDemo();
}
});
contentPane.add(jButton);
}

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

/**
* 弹窗的窗口
*/
class MyDialogDemo extends JDialog{
public MyDialogDemo() {
this.setVisible(true);
this.setBounds(300, 300, 300, 200);
// this.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);

Container contentPane = this.getContentPane();
contentPane.setLayout(null);

contentPane.add(new Label("学Swing"));
}
}

标签

label

  • 创建
1
new JLabel("xxx")
  • 实例
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
package com.kuangstudy.gui.module16;

import javax.swing.*;
import java.awt.*;

/**
* @author Qeuroal
* @date 2021-03-09 21:49
* @description 图标是一个接口,需要实现类,Frame继承
* @since
*/
public class TestIcon extends JFrame implements Icon {

public static void main(String[] args) {
// 首先生成TestIcon实例,通过这个实例再去生成新的TestIcon实例
new TestIcon().init();
}

private int width;
private int height;

public TestIcon() {}

public TestIcon(int width, int height) {
this.width = width;
this.height = height;
}

public void init() {
TestIcon testIcon = new TestIcon(30, 30);
// 图标放在标签上,也可以放在按钮上
JLabel iconTest = new JLabel("iconTest", testIcon, SwingConstants.CENTER);
Container contentPane = getContentPane();
contentPane.add(iconTest);
setVisible(true);
setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
}

@Override
public void paintIcon(Component c, Graphics g, int x, int y) {
g.fillOval(x,y,width,height);
}

@Override
public int getIconWidth() {
return width;
}

@Override
public int getIconHeight() {
return height;
}
}

Icon

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
package com.kuangstudy.gui.module16;

import javax.swing.*;
import java.awt.*;
import java.net.URL;

/**
* @author Qeuroal
* @date 2021-03-09 22:14
* @description
* @since
*/
public class TestImageIcon extends JFrame {
public TestImageIcon() {
JLabel imageIconLabel = new JLabel("ImageIcon");
// 获取图片的地址
System.out.println(TestImageIcon.class);
URL resourceURL = TestImageIcon.class.getResource("/resource/xly2.png");
// 命名不要冲突了
ImageIcon imageIcon = new ImageIcon(resourceURL);

imageIconLabel.setIcon(imageIcon);
imageIconLabel.setHorizontalAlignment(SwingConstants.CENTER);

Container container = getContentPane();
container.add(imageIconLabel);

setVisible(true);
setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
setBounds(100, 100, 300, 300);

}


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

面板

JPanel

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
package com.kuangstudy.gui.module17;

import javax.swing.*;
import java.awt.*;

/**
* @author Qeuroal
* @date 2021-03-09 22:36
* @description
* @since
*/
public class TestJPanel extends JFrame {
public TestJPanel() {
Container container = getContentPane();
//后面参数的意思是间距
container.setLayout(new GridLayout(2, 1, 10, 10));

JPanel panel1 = new JPanel(new GridLayout(1, 3));
JPanel panel2 = new JPanel(new GridLayout(1, 2));
JPanel panel3 = new JPanel(new GridLayout(2, 2));

panel1.add(new JButton("1"));
panel1.add(new JButton("1"));
panel1.add(new JButton("1"));
panel2.add(new JButton("2"));
panel2.add(new JButton("2"));
panel3.add(new JButton("3"));
panel3.add(new JButton("3"));
panel3.add(new JButton("3"));
panel3.add(new JButton("3"));

container.add(panel1);
container.add(panel2);
container.add(panel3);

setVisible(true);
setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
setBounds(300, 300, 400, 300);
}

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

JScrollPanel

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
package com.kuangstudy.gui.module17;

import javax.swing.*;
import java.awt.*;

/**
* @author Qeuroal
* @date 2021-03-09 22:45
* @description
* @since
*/
public class TestJScrollPanel extends JFrame {
public static void main(String[] args) {
new TestJScrollPanel();
}

public TestJScrollPanel() {
Container container = getContentPane();

// 文本域
JTextArea jTextArea = new JTextArea(20, 50);
jTextArea.setText("请输入文本");

// Scroll面板
JScrollPane jScrollPane = new JScrollPane(jTextArea);
container.add(jScrollPane);

setVisible(true);
setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
setBounds(300, 300, 400, 30);
}
}

按钮

图片按钮

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

import javax.swing.*;
import java.awt.*;
import java.net.URL;

/**
* @author Qeuroal
* @date 2021-03-15 22:34
* @description
* @since
*/
public class TestButton extends JFrame {

public TestButton() {
Container container = this.getContentPane();
// 将一个图片变为图标
URL resource = TestButton.class.getResource("/resource/xly2.png");
Icon imageIcon = new ImageIcon(resource);

// 把图标放在按钮上
JButton btn = new JButton();
btn.setIcon(imageIcon);
btn.setToolTipText("图片按钮");

// add
container.add(btn);
this.setVisible(true);
this.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
this.setBounds(300, 300, 400, 300);
}

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

单选按钮

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
package com.kuangstudy.gui.module18;

import javax.swing.*;
import java.awt.*;
import java.net.URL;

/**
* @author Qeuroal
* @date 2021-03-15 22:42
* @description
* @since
*/
public class TestButton2 extends JFrame {

public TestButton2() {
Container container = this.getContentPane();
// 将一个图片变为图标
URL resource = TestButton.class.getResource("/resource/xly2.png");
Icon imageIcon = new ImageIcon(resource);

// 单选框
JRadioButton jRadioButton1 = new JRadioButton("JRadioButton1");
JRadioButton jRadioButton2 = new JRadioButton("JRadioButton2");
JRadioButton jRadioButton3 = new JRadioButton("JRadioButton3");

// 由于单选框只能选个一个,所以:分组,一个组中只能选一个
ButtonGroup buttonGroup = new ButtonGroup();
buttonGroup.add(jRadioButton1);
buttonGroup.add(jRadioButton2);
buttonGroup.add(jRadioButton3);

container.add(jRadioButton1, BorderLayout.CENTER);
container.add(jRadioButton2, BorderLayout.NORTH);
container.add(jRadioButton3, BorderLayout.SOUTH);

this.setVisible(true);
this.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
this.setBounds(300, 300, 400, 300);
}

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

复选按钮

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

import javax.swing.*;
import java.awt.*;
import java.net.URL;

/**
* @author Qeuroal
* @date 2021-03-15 22:48
* @description
* @since
*/
public class TestButton3 extends JFrame {

public TestButton3() {
Container container = this.getContentPane();
// 将一个图片变为图标
URL resource = TestButton.class.getResource("/resource/xly2.png");
Icon imageIcon = new ImageIcon(resource);

// 多选框
JCheckBox jCheckBox1 = new JCheckBox("jCheckBox1");
JCheckBox jCheckBox2 = new JCheckBox("jCheckBox2");

container.add(jCheckBox1, BorderLayout.NORTH);
container.add(jCheckBox2, BorderLayout.SOUTH);

this.setVisible(true);
this.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
this.setBounds(300, 300, 400, 300);
}

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

列表

下拉框

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
package com.kuangstudy.gui.module19;

import javax.swing.*;
import java.awt.*;

/**
* @author Qeuroal
* @date 2021-03-15 22:53
* @description
* @since
*/
public class TestCombobox extends JFrame {
public TestCombobox() {
super("TestCombobox");
Container container = this.getContentPane();

JComboBox status = new JComboBox();
status.addItem(null);
status.addItem("正在热播");
status.addItem("已下架");
status.addItem("即将上映");

container.add(status);

setVisible(true);
setBounds(300, 300, 500, 300);
setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
}

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

列表框

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
package com.kuangstudy.gui.module19;

import javax.swing.*;
import java.awt.*;

/**
* @author Qeuroal
* @date 2021-03-15 23:03
* @description
* @since
*/
public class TestCombobox2 extends JFrame {
public TestCombobox2() {
super("TestCombobox");
Container container = this.getContentPane();

// 生成列表的内容
String[] contents = {"1", "2", "3"};

JList jList = new JList(contents);
container.add(jList);

setVisible(true);
setBounds(300, 300, 500, 300);
setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
}

public static void main(String[] args) {
new TestCombobox2();
}
}
  • 应用场景
    • 下拉框:选择地区,或者一些单个选项
    • 列表框:展示信息,一般是动态扩容

文本框

文本框

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
package com.kuangstudy.gui.module20;

import javax.swing.*;
import java.awt.*;

/**
* @author Qeuroal
* @date 2021-03-15 23:11
* @description
* @since
*/
public class TestText extends JFrame {
public TestText() {
super("TestCombobox");
Container container = this.getContentPane();
container.setLayout(null);

JTextField jTextField1 = new JTextField("hello");
JTextField jTextField2 = new JTextField("world", 20);

// 东西南北中布局,会自动填充满
container.add(jTextField1, BorderLayout.NORTH);
container.add(jTextField2, BorderLayout.SOUTH);


setVisible(true);
setBounds(300, 300, 500, 300);
setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
}

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

密码框

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
package com.kuangstudy.gui.module20;

import javax.swing.*;
import java.awt.*;

/**
* @author Qeuroal
* @date 2021-03-15 23:14
* @description
* @since
*/
public class TestText2 extends JFrame {
public TestText2() {
super("TestCombobox");
Container container = this.getContentPane();

// 默认 ····
JPasswordField jPasswordField = new JPasswordField();
// 手动设置 ***
jPasswordField.setEchoChar('*');

container.add(jPasswordField);

setVisible(true);
setBounds(300, 300, 500, 300);
setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
}

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

文本域

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
package com.kuangstudy.gui.module17;

import javax.swing.*;
import java.awt.*;

/**
* @author Qeuroal
* @date 2021-03-09 22:45
* @description
* @since
*/
public class TestJScrollPanel extends JFrame {
public static void main(String[] args) {
new TestJScrollPanel();
}

public TestJScrollPanel() {
Container container = getContentPane();

// 文本域
JTextArea jTextArea = new JTextArea(20, 50);
jTextArea.setText("请输入文本");

// Scroll面板
JScrollPane jScrollPane = new JScrollPane(jTextArea);
container.add(jScrollPane);

setVisible(true);
setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
setBounds(300, 300, 400, 30);
}
}

游戏实践:贪吃蛇

如果时间片足够小,就是动画:一秒30帧(人眼就是动画了)

连起来是动画,拆开就是静态的图片。如:动漫,1秒24张画

键盘监听

定时器 Timer

代码

StartGame

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
package com.kuangstudy.gui.snake;

import javax.swing.*;

/**
* @author Qeuroal
* @date 2021-03-15 23:29
* @description 游戏的主启动类
* @since
*/
public class StartGame {
public static void main(String[] args) {
JFrame frame = new JFrame();

// 是算出来的,不能被拉伸,否则就会变形了
frame.setBounds(200, 100, 900, 720);
// 窗口大小不可变
frame.setResizable(false);
frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);

// 整车游戏界面都在面板上
frame.add(new GamePanel());

frame.setVisible(true);
}
}

GamePanel

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
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
package com.kuangstudy.gui.snake;

import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.util.Random;

/**
* @author Qeuroal
* @date 2021-03-15 23:32
* @description 游戏的面板
* @since
*/
public class GamePanel extends JPanel implements KeyListener, ActionListener {

/**
* 定义蛇的数据结构
*/
// 蛇的长度
int length;
// 蛇的x坐标 25*25
int[] snakeX = new int[100];
// 蛇的Y坐标 25*25
int[] snakeY = new int[100];
// 初始方向
String fx;
// 游戏当前状态:开始,停止
boolean isStart= false;
// 食物的坐标
int foodX;
int foodY;
Random random = new Random();
// 积分
int score;
// 游戏失败状态
boolean isFail = false;
// 定时器:ms为单位,监听this这个对象。100毫秒执行一次。
Timer timer = new Timer(100, this);
/**
* 构造器
*/
public GamePanel() {
init();
// 获得焦点事件
this.setFocusable(true);
// 获取键盘事件
this.addKeyListener(this);
// 游戏一开始定时器就启动
timer.start();
}


/**
* 初始化方法
*/
public void init() {
length = 3;
// 脑袋的坐标
snakeX[0] = 100; snakeY[0] = 100;
// 第一个身体的坐标
snakeX[1] = 75; snakeY[1] = 100;
// 第二个身体的坐标
snakeX[2] = 50; snakeY[2] = 100;
fx = "R";
// 把食物随机放在界面上
foodX = 25 + 25 * random.nextInt(34);
foodY = 75 + 25 * random.nextInt(24);
// 积分
score = 0;
}


/**
* 绘制面板,我们游戏中的所有东西,都是用这个笔来画
* @param g
*/
@Override
protected void paintComponent(Graphics g) {
// 清屏
super.paintComponent(g);
setBackground(Color.WHITE);
// 绘制静态面板,头部广告栏画上去
Data.header.paintIcon(this, g, 25, 11);
// 默认的游戏界面
g.fillRect(25, 75, 850, 600);

// 画积分
g.setColor(Color.WHITE);
g.setFont(new Font("微软雅黑", Font.BOLD, 15));
g.drawString("长度: " + length,750, 35 );
g.drawString("分数: " + score, 750, 50);

// 画食物
Data.food.paintIcon(this, g, foodX, foodY);

// 把小蛇画上去
if (fx.equals("R")) {
Data.right.paintIcon(this, g, snakeX[0], snakeY[0]);
} else if (fx.equals("L")) {
Data.left.paintIcon(this, g, snakeX[0], snakeY[0]);
} else if (fx.equals("U")) {
Data.up.paintIcon(this, g, snakeX[0], snakeY[0]);
} else if (fx.equals("D")) {
Data.down.paintIcon(this, g, snakeX[0], snakeY[0]);
}
for (int i = 1; i < length; i++) {
Data.body.paintIcon(this, g, snakeX[i], snakeY[i]);
}

// 游戏状态
if (isStart == false) {
g.setColor(Color.WHITE);
// 设置字体
g.setFont(new Font("微软雅黑", Font.BOLD, 40));
g.drawString("按下空格开始游戏", 300, 300);
}

if (isFail) {
g.setColor(Color.RED);
// 设置字体
g.setFont(new Font("微软雅黑", Font.BOLD, 40));
g.drawString("失败,按下空格重新开始游戏", 300, 300);
}
}



/**
* 键盘监听事件
* @param e
*/
@Override
public void keyPressed(KeyEvent e) {
// 获得键盘按键是哪一个
int keyCode = e.getKeyCode();
// 如果按下的是空格键
if (keyCode == KeyEvent.VK_SPACE) {
if (isFail) {
// 重新开始
isFail = false;
init();
} else {
isStart = !isStart;
}
repaint();

}
// 小蛇移动
if (keyCode == KeyEvent.VK_UP) {
fx = "U";
} else if (keyCode == KeyEvent.VK_DOWN) {
fx = "D";
} else if (keyCode == KeyEvent.VK_LEFT) {
fx = "L";
} else if (keyCode == KeyEvent.VK_RIGHT) {
fx = "R";
}
}

@Override
public void keyReleased(KeyEvent e) {
}
@Override
public void keyTyped(KeyEvent e) {
}

/**
* 事件监听——需要通过固定事件来刷新:10次/1s
* @param e
*/
@Override
public void actionPerformed(ActionEvent e) {
// 如果游戏是开始状态,就让小蛇动起来
if (isStart && isFail == false) {
// 吃食物
if (snakeX[0] == foodX && snakeY[0] == foodY) {
// 长度+1
length++;
// 分数+10
score += 10;
// 重新生成食物
foodX = 25 + 25 * random.nextInt(34);
foodY = 75 + 25 * random.nextInt(24);
}

// 移动:后一节移到前一节的位置
for (int i = length - 1; i > 0; i--) {
snakeX[i] = snakeX[i - 1];
snakeY[i] = snakeY[i - 1];
}
// 走向
if (fx.equals("R")) {
snakeX[0] += 25;
// 边界判断
if (snakeX[0] > 850) {
snakeX[0] = 25;
}
} else if (fx.equals("L")){
snakeX[0] -= 25;
if (snakeX[0] < 25) {
snakeX[0] = 850;
}
} else if (fx.equals("U")){
snakeY[0] -= 25;
if (snakeY[0] < 75) {
snakeY[0] = 650;
}
} else if (fx.equals("D")){
snakeY[0] += 25;
if (snakeY[0] > 650) {
snakeY[0] = 75;
}
}

// 失败判定:撞到自己就算失败
for (int i = 1; i < length; i++) {
if (snakeX[0] == snakeX[i] && snakeY[0] == snakeY[i]) {
isFail = true;
}
}

// 重画页面
repaint();
}
// 定时器开始
timer.start();
}
}

总结

补充

C/S:客户端+服务器 (主流:C++)

B/S:浏览器+服务器 (主流:Java)

IO

IO是指Input/Output,即输入和输出。以内存为中心:

  • Input指从外部读入数据到内存,例如,把文件从磁盘读取到内存,从网络读取数据到内存等等。
  • Output指把数据从内存输出到外部,例如,把数据从内存写入到文件,把数据从内存输出到网络等等。

为什么要把数据读到内存才能处理这些数据?因为代码是在内存中运行的,数据也必须读到内存,最终的表示方式无非是byte数组,字符串等,都必须存放在内存里。
从Java代码来看,输入实际上就是从外部,例如,硬盘上的某个文件,把内容读到内存,并且以Java提供的某种数据类型表示,例如,byte[],String,这样,后续代码才能处理这些数据。
因为内存有“易失性”的特点,所以必须把处理后的数据以某种方式输出,例如,写入到文件。Output实际上就是把Java表示的数据格式,例如,byte[],String等输出到某个地方。
IO流是一种顺序读写数据的模式,它的特点是单向流动。数据类似自来水一样在水管中流动,所以我们把它称为IO流。

InputStream / OutputStream

IO流以byte(字节)为最小单位,因此也称为字节流。例如,我们要从磁盘读入一个文件,包含6个字节,就相当于读入了6个字节的数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
╔════════════╗
║ Memory ║
╚════════════╝

│0x48
│0x65
│0x6c
│0x6c
│0x6f
│0x21
╔═══════════╗
║ Hard Disk ║
╚═══════════╝

这6个字节是按顺序读入的,所以是输入字节流。
反过来,我们把6个字节从内存写入磁盘文件,就是输出字节流:
1
2
3
4
5
6
7
8
9
10
11
12
13
╔════════════╗
║ Memory ║
╚════════════╝
│0x21
│0x6f
│0x6c
│0x6c
│0x65
│0x48

╔═══════════╗
║ Hard Disk ║
╚═══════════╝

在Java中,InputStream代表输入字节流,OuputStream代表输出字节流,这是最基本的两种IO流。

Reader / Writer

如果我们需要读写的是字符,并且字符不全是单字节表示的ASCII字符,那么,按照char来读写显然更方便,这种流称为字符流。
Java提供了Reader和Writer表示字符流,字符流传输的最小数据单位是char。
例如,我们把char[]数组Hi你好这4个字符用Writer字符流写入文件,并且使用UTF-8编码,得到的最终文件内容是8个字节,英文字符H和i各占一个字节,中文字符你好各占3个字节:

1
2
3
4
0x48
0x69
0xe4bda0
0xe5a5bd

反过来,我们用Reader读取以UTF-8编码的这8个字节,会从Reader中得到Hi你好这4个字符。
因此,Reader和Writer本质上是一个能自动编解码的InputStream和OutputStream。
使用Reader,数据源虽然是字节,但我们读入的数据都是char类型的字符,原因是Reader内部把读入的byte做了解码,转换成了char。使用InputStream,我们读入的数据和原始二进制数据一模一样,是byte[]数组,但是我们可以自己把二进制byte[]数组按照某种编码转换为字符串。究竟使用Reader还是InputStream,要取决于具体的使用场景。如果数据源不是文本,就只能使用InputStream,如果数据源是文本,使用Reader更方便一些。Writer和OutputStream是类似的。

同步和异步

同步IO是指,读写IO时代码必须等待数据返回后才继续执行后续代码,它的优点是代码编写简单,缺点是CPU执行效率低。
而异步IO是指,读写IO时仅发出请求,然后立刻执行后续代码,它的优点是CPU执行效率高,缺点是代码编写复杂。
Java标准库的包java.io提供了同步IO,而java.nio则是异步IO。上面我们讨论的InputStream、OutputStream、Reader和Writer都是同步IO的抽象类,对应的具体实现类,以文件为例,有FileInputStream、FileOutputStream、FileReader和FileWriter。
本节我们只讨论Java的同步IO,即输入/输出流的IO模型。

小结

  • IO流是一种流式的数据输入/输出模型:
    • 二进制数据以byte为最小单位在InputStream/OutputStream中单向流动;
    • 字符数据以char为最小单位在Reader/Writer中单向流动。
  • Java标准库的java.io包提供了同步IO功能:
    • 字节流接口:InputStream/OutputStream;
    • 字符流接口:Reader/Writer。

File对象

在计算机系统中,文件是非常重要的存储方式。Java的标准库java.io提供了File对象来操作文件和目录。
要构造一个File对象,需要传入文件路径:

1
2
3
4
5
6
7
import java.io.*;
public class Main {
public static void main(String[] args) {
File f = new File("C:\\Windows\\notepad.exe");
System.out.println(f);
}
}

构造File对象时,既可以传入绝对路径,也可以传入相对路径。绝对路径是以根目录开头的完整路径,例如:
1
File f = new File("C:\\Windows\\notepad.exe");

注意Windows平台使用\作为路径分隔符,在Java字符串中需要用\表示一个\。Linux平台使用/作为路径分隔符:
1
File f = new File("/usr/bin/javac");

传入相对路径时,相对路径前面加上当前目录就是绝对路径:
1
2
3
4
// 假设当前目录是C:\Docs
File f1 = new File("sub\\javac"); // 绝对路径是C:\Docs\sub\javac
File f3 = new File(".\\sub\\javac"); // 绝对路径是C:\Docs\sub\javac
File f3 = new File("..\\sub\\javac"); // 绝对路径是C:\sub\javac

可以用.表示当前目录,..表示上级目录。
File对象有3种形式表示的路径,一种是getPath(),返回构造方法传入的路径,一种是getAbsolutePath(),返回绝对路径,一种是getCanonicalPath,它和绝对路径类似,但是返回的是规范路径。
什么是规范路径?我们看以下代码:
1
2
3
4
5
6
7
8
9
import java.io.*;
public class Main {
public static void main(String[] args) throws IOException {
File f = new File("..");
System.out.println(f.getPath());
System.out.println(f.getAbsolutePath());
System.out.println(f.getCanonicalPath());
}
}

绝对路径可以表示成C:\Windows\System32\..\notepad.exe,而规范路径就是把...转换成标准的绝对路径后的路径:C:\Windows\notepad.exe
因为Windows和Linux的路径分隔符不同,File对象有一个静态变量用于表示当前平台的系统分隔符:
1
System.out.println(File.separator); // 根据当前平台打印"\"或"/"

文件和目录

File对象既可以表示文件,也可以表示目录。特别要注意的是,构造一个File对象,即使传入的文件或目录不存在,代码也不会出错,因为构造一个File对象,并不会导致任何磁盘操作。只有当我们调用File对象的某些方法的时候,才真正进行磁盘操作。

例如,调用isFile(),判断该File对象是否是一个已存在的文件,调用isDirectory(),判断该File对象是否是一个已存在的目录:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import java.io.*;

public class Main {
public static void main(String[] args) throws IOException {
File f1 = new File("C:\\Windows");
File f2 = new File("C:\\Windows\\notepad.exe");
File f3 = new File("C:\\Windows\\nothing");
System.out.println(f1.isFile());
System.out.println(f1.isDirectory());
System.out.println(f2.isFile());
System.out.println(f2.isDirectory());
System.out.println(f3.isFile());
System.out.println(f3.isDirectory());
}
}

用File对象获取到一个文件时,还可以进一步判断文件的权限和大小:

  • boolean canRead():是否可读;
  • boolean canWrite():是否可写;
  • boolean canExecute():是否可执行;
  • long length():文件字节大小。

对目录而言,是否可执行表示能否列出它包含的文件和子目录。

创建和删除文件

当File对象表示一个文件时,可以通过createNewFile()创建一个新文件,用delete()删除该文件:

1
2
3
4
5
6
7
8
File file = new File("/path/to/file");
if (file.createNewFile()) {
// 文件创建成功:
// TODO:
if (file.delete()) {
// 删除文件成功:
}
}

有些时候,程序需要读写一些临时文件,File对象提供了createTempFile()来创建一个临时文件,以及deleteOnExit()在JVM退出时自动删除该文件。
1
2
3
4
5
6
7
8
9
10
import java.io.*;

public class Main {
public static void main(String[] args) throws IOException {
File f = File.createTempFile("tmp-", ".txt"); // 提供临时文件的前缀和后缀
f.deleteOnExit(); // JVM退出时自动删除
System.out.println(f.isFile());
System.out.println(f.getAbsolutePath());
}
}

遍历文件和目录

当File对象表示一个目录时,可以使用list()和listFiles()列出目录下的文件和子目录名。listFiles()提供了一系列重载方法,可以过滤不想要的文件和目录:

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
import java.io.*;

public class Main {
public static void main(String[] args) throws IOException {
File f = new File("C:\\Windows");
File[] fs1 = f.listFiles(); // 列出所有文件和子目录
printFiles(fs1);
File[] fs2 = f.listFiles(new FilenameFilter() { // 仅列出.exe文件
public boolean accept(File dir, String name) {
return name.endsWith(".exe"); // 返回true表示接受该文件
}
});
printFiles(fs2);
}

static void printFiles(File[] files) {
System.out.println("==========");
if (files != null) {
for (File f : files) {
System.out.println(f);
}
}
System.out.println("==========");
}
}

list() : 获取该目录下的所有文件
listFiles(): 获取该目录下所有文件的绝对路径

和文件操作类似,File对象如果表示一个目录,可以通过以下方法创建和删除目录:

  • boolean mkdir():创建当前File对象表示的目录;
  • boolean mkdirs():创建当前File对象表示的目录,并在必要时将不存在的父目录也创建出来;
  • boolean delete():删除当前File对象表示的目录,当前目录必须为空才能删除成功。

Path

Java标准库还提供了一个Path对象,它位于java.nio.file包。Path对象和File对象类似,但操作更加简单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import java.io.*;
import java.nio.file.*;

public class Main {
public static void main(String[] args) throws IOException {
Path p1 = Paths.get(".", "project", "study"); // 构造一个Path对象
System.out.println(p1);
Path p2 = p1.toAbsolutePath(); // 转换为绝对路径
System.out.println(p2);
Path p3 = p2.normalize(); // 转换为规范路径
System.out.println(p3);
File f = p3.toFile(); // 转换为File对象
System.out.println(f);
for (Path p : Paths.get("..").toAbsolutePath()) { // 可以直接遍历Path
System.out.println(" " + p);
}
}
}

如果需要对目录进行复杂的拼接、遍历等操作,使用Path对象更方便。

小结

Java标准库的java.io.File对象表示一个文件或者目录:

  • 创建File对象本身不涉及IO操作;
  • 可以获取路径/绝对路径/规范路径:getPath()/getAbsolutePath()/getCanonicalPath();
  • 可以获取目录的文件和子目录:list()/listFiles();
  • 可以创建或删除文件和目录。

InputStream

InputStream就是Java标准库提供的最基本的输入流。它位于java.io这个包里。java.io包提供了所有同步IO的功能。
要特别注意的一点是,InputStream并不是一个接口,而是一个抽象类,它是所有输入流的超类。这个抽象类定义的一个最重要的方法就是int read(),签名如下:

1
public abstract int read() throws IOException;

这个方法会读取输入流的下一个字节,并返回字节表示的int值(0~255)。如果已读到末尾,返回-1表示不能继续读取了。
FileInputStream是InputStream的一个子类。顾名思义,FileInputStream就是从文件流中读取数据。下面的代码演示了如何完整地读取一个FileInputStream的所有字节:
1
2
3
4
5
6
7
8
9
10
11
12
public void readFile() throws IOException {
// 创建一个FileInputStream对象:
InputStream input = new FileInputStream("src/readme.txt");
for (;;) {
int n = input.read(); // 反复调用read()方法,直到返回-1
if (n == -1) {
break;
}
System.out.println(n); // 打印byte的值
}
input.close(); // 关闭流
}

在计算机中,类似文件、网络端口这些资源,都是由操作系统统一管理的。应用程序在运行的过程中,如果打开了一个文件进行读写,完成后要及时地关闭,以便让操作系统把资源释放掉,否则,应用程序占用的资源会越来越多,不但白白占用内存,还会影响其他应用程序的运行。
InputStream和OutputStream都是通过close()方法来关闭流。关闭流就会释放对应的底层资源。
我们还要注意到在读取或写入IO流的过程中,可能会发生错误,例如,文件不存在导致无法读取,没有写权限导致写入失败,等等,这些底层错误由Java虚拟机自动封装成IOException异常并抛出。因此,所有与IO操作相关的代码都必须正确处理IOException。
仔细观察上面的代码,会发现一个潜在的问题:如果读取过程中发生了IO错误,InputStream就没法正确地关闭,资源也就没法及时释放。
因此,我们需要用try … finally来保证InputStream在无论是否发生IO错误的时候都能够正确地关闭:
1
2
3
4
5
6
7
8
9
10
11
12
public void readFile() throws IOException {
InputStream input = null;
try {
input = new FileInputStream("src/readme.txt");
int n;
while ((n = input.read()) != -1) { // 利用while同时读取并判断
System.out.println(n);
}
} finally {
if (input != null) { input.close(); }
}
}

用try … finally来编写上述代码会感觉比较复杂,更好的写法是利用Java 7引入的新的try(resource)的语法,只需要编写try语句,让编译器自动为我们关闭资源。推荐的写法如下:
1
2
3
4
5
6
7
8
public void readFile() throws IOException {
try (InputStream input = new FileInputStream("src/readme.txt")) {
int n;
while ((n = input.read()) != -1) {
System.out.println(n);
}
} // 编译器在此自动为我们写入finally并调用close()
}

实际上,编译器并不会特别地为InputStream加上自动关闭。编译器只看try(resource = ...)中的对象是否实现了java.lang.AutoCloseable接口,如果实现了,就自动加上finally语句并调用close()方法。InputStream和OutputStream都实现了这个接口,因此,都可以用在try(resource)中。

缓冲

在读取流的时候,一次读取一个字节并不是最高效的方法。很多流支持一次性读取多个字节到缓冲区,对于文件和网络流来说,利用缓冲区一次性读取多个字节效率往往要高很多。InputStream提供了两个重载方法来支持读取多个字节:

  • int read(byte[] b):读取若干字节并填充到byte[]数组,返回读取的字节数
  • int read(byte[] b, int off, int len):指定byte[]数组的偏移量和最大填充数

    off:在数组b在其中写入数据的起始位置的偏移;
    len: 要读取的字节数。
    要满足 off+len <= b的大小
    缓冲相当于多次执行 input.read(),因此在循环中,如果在下标小于偏移量的数组,是不读取的。

利用上述方法一次读取多个字节时,需要先定义一个byte[]数组作为缓冲区,read()方法会尽可能多地读取字节到缓冲区, 但不会超过缓冲区的大小。read()方法的返回值不再是字节的int值,而是返回实际读取了多少个字节。如果返回-1,表示没有更多的数据了。
利用缓冲区一次读取多个字节的代码如下:

1
2
3
4
5
6
7
8
9
10
public void readFile() throws IOException {
try (InputStream input = new FileInputStream("src/readme.txt")) {
// 定义1000个字节大小的缓冲区:
byte[] buffer = new byte[1000];
int n;
while ((n = input.read(buffer)) != -1) { // 读取到缓冲区
System.out.println("read " + n + " bytes.");
}
}
}

阻塞

在调用InputStream的read()方法读取数据时,我们说read()方法是阻塞(Blocking)的。它的意思是,对于下面的代码:

1
2
3
int n;
n = input.read(); // 必须等待read()方法返回才能执行下一行代码
int m = n;

执行到第二行代码时,必须等read()方法返回后才能继续。因为读取IO流相比执行普通代码,速度会慢很多,因此,无法确定read()方法调用到底要花费多长时间。

InputStream实现类

用FileInputStream可以从文件获取输入流,这是InputStream常用的一个实现类。此外,ByteArrayInputStream可以在内存中模拟一个InputStream:

1
2
3
4
5
6
7
8
9
10
11
12
13
import java.io.*;

public class Main {
public static void main(String[] args) throws IOException {
byte[] data = { 72, 101, 108, 108, 111, 33 };
try (InputStream input = new ByteArrayInputStream(data)) {
int n;
while ((n = input.read()) != -1) {
System.out.println((char)n);
}
}
}
}

ByteArrayInputStream实际上是把一个byte[]数组在内存中变成一个InputStream,虽然实际应用不多,但测试的时候,可以用它来构造一个InputStream。
举个栗子:我们想从文件中读取所有字节,并转换成char然后拼成一个字符串,可以这么写:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Main {
public static void main(String[] args) throws IOException {
String s;
try (InputStream input = new FileInputStream("C:\\test\\README.txt")) {
int n;
StringBuilder sb = new StringBuilder();
while ((n = input.read()) != -1) {
sb.append((char) n);
}
s = sb.toString();
}
System.out.println(s);
}
}

要测试上面的程序,就真的需要在本地硬盘上放一个真实的文本文件。如果我们把代码稍微改造一下,提取一个readAsString()的方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Main {
public static void main(String[] args) throws IOException {
String s;
try (InputStream input = new FileInputStream("C:\\test\\README.txt")) {
s = readAsString(input);
}
System.out.println(s);
}

public static String readAsString(InputStream input) throws IOException {
int n;
StringBuilder sb = new StringBuilder();
while ((n = input.read()) != -1) {
sb.append((char) n);
}
return sb.toString();
}
}

对这个String readAsString(InputStream input)方法进行测试就相当简单,因为不一定要传入一个真的FileInputStream:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import java.io.*;
public class Main {
public static void main(String[] args) throws IOException {
byte[] data = { 72, 101, 108, 108, 111, 33 };
try (InputStream input = new ByteArrayInputStream(data)) {
String s = readAsString(input);
System.out.println(s);
}
}

public static String readAsString(InputStream input) throws IOException {
int n;
StringBuilder sb = new StringBuilder();
while ((n = input.read()) != -1) {
sb.append((char) n);
}
return sb.toString();
}
}

这就是面向抽象编程原则的应用:接受InputStream抽象类型,而不是具体的FileInputStream类型,从而使得代码可以处理InputStream的任意实现类。

小结

  • Java标准库的java.io.InputStream定义了所有输入流的超类:
    • FileInputStream实现了文件流输入;
    • ByteArrayInputStream在内存中模拟一个字节流输入。
  • 总是使用try(resource)来保证InputStream正确关闭。

OutputStream

和InputStream相反,OutputStream是Java标准库提供的最基本的输出流。
和InputStream类似,OutputStream也是抽象类,它是所有输出流的超类。这个抽象类定义的一个最重要的方法就是void write(int b),签名如下:

1
public abstract void write(int b) throws IOException;

这个方法会写入一个字节到输出流。要注意的是,虽然传入的是int参数,但只会写入一个字节,即只写入int最低8位表示字节的部分(相当于b & 0xff)。
和InputStream类似,OutputStream也提供了close()方法关闭输出流,以便释放系统资源。要特别注意:OutputStream还提供了一个flush()方法,它的目的是将缓冲区的内容真正输出到目的地。
为什么要有flush()?因为向磁盘、网络写入数据的时候,出于效率的考虑,操作系统并不是输出一个字节就立刻写入到文件或者发送到网络,而是把输出的字节先放到内存的一个缓冲区里(本质上就是一个byte[]数组),等到缓冲区写满了,再一次性写入文件或者网络。对于很多IO设备来说,一次写一个字节和一次写1000个字节,花费的时间几乎是完全一样的,所以OutputStream有个flush()方法,能强制把缓冲区内容输出。
通常情况下,我们不需要调用这个flush()方法,因为缓冲区写满了OutputStream会自动调用它,并且,在调用close()方法关闭OutputStream之前,也会自动调用flush()方法。
但是,在某些情况下,我们必须手动调用flush()方法。举个栗子:
小明正在开发一款在线聊天软件,当用户输入一句话后,就通过OutputStream的write()方法写入网络流。小明测试的时候发现,发送方输入后,接收方根本收不到任何信息,怎么肥四?
原因就在于写入网络流是先写入内存缓冲区,等缓冲区满了才会一次性发送到网络。如果缓冲区大小是4K,则发送方要敲几千个字符后,操作系统才会把缓冲区的内容发送出去,这个时候,接收方会一次性收到大量消息。
解决办法就是每输入一句话后,立刻调用flush(),不管当前缓冲区是否已满,强迫操作系统把缓冲区的内容立刻发送出去。
实际上,InputStream也有缓冲区。例如,从FileInputStream读取一个字节时,操作系统往往会一次性读取若干字节到缓冲区,并维护一个指针指向未读的缓冲区。然后,每次我们调用int read()读取下一个字节时,可以直接返回缓冲区的下一个字节,避免每次读一个字节都导致IO操作。当缓冲区全部读完后继续调用read(),则会触发操作系统的下一次读取并再次填满缓冲区。

FileOutputStream

我们以FileOutputStream为例,演示如何将若干个字节写入文件流:

1
2
3
4
5
6
7
8
9
public void writeFile() throws IOException {
OutputStream output = new FileOutputStream("out/readme.txt");
output.write(72); // H
output.write(101); // e
output.write(108); // l
output.write(108); // l
output.write(111); // o
output.close();
}

每次写入一个字节非常麻烦,更常见的方法是一次性写入若干字节。这时,可以用OutputStream提供的重载方法void write(byte[])来实现:
1
2
3
4
5
public void writeFile() throws IOException {
OutputStream output = new FileOutputStream("out/readme.txt");
output.write("Hello".getBytes("UTF-8")); // Hello
output.close();
}

和InputStream一样,上述代码没有考虑到在发生异常的情况下如何正确地关闭资源。写入过程也会经常发生IO错误,例如,磁盘已满,无权限写入等等。我们需要用try(resource)来保证OutputStream在无论是否发生IO错误的时候都能够正确地关闭:
1
2
3
4
5
public void writeFile() throws IOException {
try (OutputStream output = new FileOutputStream("out/readme.txt")) {
output.write("Hello".getBytes("UTF-8")); // Hello
} // 编译器在此自动为我们写入finally并调用close()
}

阻塞

和InputStream一样,OutputStream的write()方法也是阻塞的。

OutputStream实现类

用FileOutputStream可以从文件获取输出流,这是OutputStream常用的一个实现类。此外,ByteArrayOutputStream可以在内存中模拟一个OutputStream:

1
2
3
4
5
6
7
8
9
10
11
12
13
import java.io.*;

public class Main {
public static void main(String[] args) throws IOException {
byte[] data;
try (ByteArrayOutputStream output = new ByteArrayOutputStream()) {
output.write("Hello ".getBytes("UTF-8"));
output.write("world!".getBytes("UTF-8"));
data = output.toByteArray();
}
System.out.println(new String(data, "UTF-8"));
}
}

ByteArrayOutputStream实际上是把一个byte[]数组在内存中变成一个OutputStream,虽然实际应用不多,但测试的时候,可以用它来构造一个OutputStream。
同时操作多个AutoCloseable资源时,在try(resource) { ... }语句中可以同时写出多个资源,用;隔开。例如,同时读写两个文件:
1
2
3
4
5
6
// 读取input.txt,写入output.txt:
try (InputStream input = new FileInputStream("input.txt");
OutputStream output = new FileOutputStream("output.txt"))
{
input.transferTo(output); // transferTo的作用是?
}

小结

  • Java标准库的java.io.OutputStream定义了所有输出流的超类:
    • FileOutputStream实现了文件流输出;
    • ByteArrayOutputStream在内存中模拟一个字节流输出。
  • 某些情况下需要手动调用OutputStream的flush()方法来强制输出缓冲区。
  • 总是使用try(resource)来保证OutputStream正确关闭。

Filter模式

Java的IO标准库提供的InputStream根据来源可以包括:

  • FileInputStream:从文件读取数据,是最终数据源;
  • ServletInputStream:从HTTP请求读取数据,是最终数据源;
  • Socket.getInputStream():从TCP连接读取数据,是最终数据源;

如果我们要给FileInputStream添加缓冲功能,则可以从FileInputStream派生一个类:

1
BufferedFileInputStream extends FileInputStream

如果要给FileInputStream添加计算签名的功能,类似的,也可以从FileInputStream派生一个类:
1
DigestFileInputStream extends FileInputStream

如果要给FileInputStream添加加密/解密功能,还是可以从FileInputStream派生一个类:
1
CipherFileInputStream extends FileInputStream

如果要给FileInputStream添加缓冲和签名的功能,那么我们还需要派生BufferedDigestFileInputStream。如果要给FileInputStream添加缓冲和加解密的功能,则需要派生BufferedCipherFileInputStream。
我们发现,给FileInputStream添加3种功能,至少需要3个子类。这3种功能的组合,又需要更多的子类:
1
2
3
4
5
6
7
8
9
10
11
12
13
                         ┌─────────────────┐
│ FileInputStream │
└─────────────────┘

┌───────────┬─────────┼─────────┬───────────┐
│ │ │ │ │
┌───────────────────────┐│┌─────────────────┐│┌─────────────────────┐
│BufferedFileInputStream│││DigestInputStream│││CipherFileInputStream│
└───────────────────────┘│└─────────────────┘│└─────────────────────┘
│ │
┌─────────────────────────────┐ ┌─────────────────────────────┐
│BufferedDigestFileInputStream│ │BufferedCipherFileInputStream│
└─────────────────────────────┘ └─────────────────────────────┘

这还只是针对FileInputStream设计,如果针对另一种InputStream设计,很快会出现子类爆炸的情况。
因此,直接使用继承,为各种InputStream附加更多的功能,根本无法控制代码的复杂度,很快就会失控。
为了解决依赖继承会导致子类数量失控的问题,JDK首先将InputStream分为两大类:

一类是直接提供数据的基础InputStream,例如:

  • FileInputStream
  • ByteArrayInputStream
  • ServletInputStream

一类是提供额外附加功能的InputStream,例如:

  • BufferedInputStream
  • DigestInputStream
  • CipherInputStream

当我们需要给一个“基础”InputStream附加各种功能时,我们先确定这个能提供数据源的InputStream,因为我们需要的数据总得来自某个地方,例如,FileInputStream,数据来源自文件:

1
InputStream file = new FileInputStream("test.gz");

紧接着,我们希望FileInputStream能提供缓冲的功能来提高读取的效率,因此我们用BufferedInputStream包装这个InputStream,得到的包装类型是BufferedInputStream,但它仍然被视为一个InputStream:
1
InputStream buffered = new BufferedInputStream(file);

无论我们包装多少次,得到的对象始终是InputStream,我们直接用InputStream来引用它,就可以正常读取:
1
2
3
4
5
6
7
8
9
┌─────────────────────────┐
│GZIPInputStream │
│┌───────────────────────┐│
││BufferedFileInputStream││
││┌─────────────────────┐││
│││ FileInputStream │││
││└─────────────────────┘││
│└───────────────────────┘│
└─────────────────────────┘

上述这种通过一个“基础”组件再叠加各种“附加”功能组件的模式,称之为Filter模式(或者装饰器模式:Decorator)。它可以让我们通过少量的类来实现各种功能的组合:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
                  ┌─────────────┐
│ InputStream │
└─────────────┘
▲ ▲
┌────────────────────┐ │ │ ┌─────────────────┐
│ FileInputStream │─┤ └─│FilterInputStream│
└────────────────────┘ │ └─────────────────┘
┌────────────────────┐ │ ▲ ┌───────────────────┐
│ByteArrayInputStream│─┤ ├─│BufferedInputStream│
└────────────────────┘ │ │ └───────────────────┘
┌────────────────────┐ │ │ ┌───────────────────┐
│ ServletInputStream │─┘ ├─│ DataInputStream │
└────────────────────┘ │ └───────────────────┘
│ ┌───────────────────┐
└─│CheckedInputStream │
└───────────────────┘

类似的,OutputStream也是以这种模式来提供各种功能:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
                  ┌─────────────┐
│OutputStream │
└─────────────┘
▲ ▲
┌─────────────────────┐ │ │ ┌──────────────────┐
│ FileOutputStream │─┤ └─│FilterOutputStream│
└─────────────────────┘ │ └──────────────────┘
┌─────────────────────┐ │ ▲ ┌────────────────────┐
│ByteArrayOutputStream│─┤ ├─│BufferedOutputStream│
└─────────────────────┘ │ │ └────────────────────┘
┌─────────────────────┐ │ │ ┌────────────────────┐
│ ServletOutputStream │─┘ ├─│ DataOutputStream │
└─────────────────────┘ │ └────────────────────┘
│ ┌────────────────────┐
└─│CheckedOutputStream │
└────────────────────┘

编写FilterInputStream

我们也可以自己编写FilterInputStream,以便可以把自己的FilterInputStream“叠加”到任何一个InputStream中。
下面的例子演示了如何编写一个CountInputStream,它的作用是对输入的字节进行计数:

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
import java.io.*;

public class Main {
public static void main(String[] args) throws IOException {
byte[] data = "hello, world!".getBytes("UTF-8");
try (CountInputStream input = new CountInputStream(new ByteArrayInputStream(data))) {
int n;
while ((n = input.read()) != -1) {
System.out.println((char)n);
}
System.out.println("Total read " + input.getBytesRead() + " bytes");
}
}
}

class CountInputStream extends FilterInputStream {
private int count = 0;

CountInputStream(InputStream in) {
super(in);
}

public int getBytesRead() {
return this.count;
}

public int read() throws IOException {
int n = in.read();
if (n != -1) {
this.count ++;
}
return n;
}

public int read(byte[] b, int off, int len) throws IOException {
int n = in.read(b, off, len);
if (n != -1) {
this.count += n;
}
return n;
}
}

注意到在叠加多个FilterInputStream,我们只需要持有最外层的InputStream,并且,当最外层的InputStream关闭时(在try(resource)块的结束处自动关闭),内层的InputStream的close()方法也会被自动调用,并最终调用到最核心的“基础”InputStream,因此不存在资源泄露。

小结

  • Java的IO标准库使用Filter模式为InputStream和OutputStream增加功能:
    • 可以把一个InputStream和任意个FilterInputStream组合;
    • 可以把一个OutputStream和任意个FilterOutputStream组合。
  • Filter模式可以在运行期动态增加功能(又称Decorator模式)。

操作Zip

ZipInputStream是一种FilterInputStream,它可以直接读取zip包的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
┌───────────────────┐
│ InputStream │
└───────────────────┘


┌───────────────────┐
│ FilterInputStream │
└───────────────────┘


┌───────────────────┐
│InflaterInputStream│
└───────────────────┘


┌───────────────────┐
│ ZipInputStream │
└───────────────────┘


┌───────────────────┐
│ JarInputStream │
└───────────────────┘

另一个JarInputStream是从ZipInputStream派生,它增加的主要功能是直接读取jar文件里面的MANIFEST.MF文件。因为本质上jar包就是zip包,只是额外附加了一些固定的描述文件。

读取zip包

我们来看看ZipInputStream的基本用法。
我们要创建一个ZipInputStream,通常是传入一个FileInputStream作为数据源,然后,循环调用getNextEntry(),直到返回null,表示zip流结束。
一个ZipEntry表示一个压缩文件或目录,如果是压缩文件,我们就用read()方法不断读取,直到返回-1:

1
2
3
4
5
6
7
8
9
10
11
try (ZipInputStream zip = new ZipInputStream(new FileInputStream(...))) {
ZipEntry entry = null;
while ((entry = zip.getNextEntry()) != null) {
String name = entry.getName();
if (!entry.isDirectory()) {
int n;
while ((n = zip.read()) != -1) {
...
}
}
}

写入zip包

ZipOutputStream是一种FilterOutputStream,它可以直接写入内容到zip包。我们要先创建一个ZipOutputStream,通常是包装一个FileOutputStream,然后,每写入一个文件前,先调用putNextEntry(),然后用write()写入byte[]数据,写入完毕后调用closeEntry()结束这个文件的打包:

1
2
3
4
5
6
7
8
try (ZipOutputStream zip = new ZipOutputStream(new FileOutputStream(...))) {
File[] files = ...
for (File file : files) {
zip.putNextEntry(new ZipEntry(file.getName()));
zip.write(getFileDataAsBytes(file));
zip.closeEntry();
}
}

上面的代码没有考虑文件的目录结构。如果要实现目录层次结构,new ZipEntry(name)传入的name要用相对路径。

小结

  • ZipInputStream可以读取zip格式的流,ZipOutputStream可以把多份数据写入zip包;
  • 配合FileInputStream和FileOutputStream就可以读写zip文件。

读取classpath资源

很多Java程序启动的时候,都需要读取配置文件。例如,从一个.properties文件中读取配置:

1
2
3
4
String conf = "C:\\conf\\default.properties";
try (InputStream input = new FileInputStream(conf)) {
// TODO:
}

这段代码要正常执行,必须在C盘创建conf目录,然后在目录里创建default.properties文件。但是,在Linux系统上,路径和Windows的又不一样。
因此,从磁盘的固定目录读取配置文件,不是一个好的办法。
有没有路径无关的读取文件的方式呢?
我们知道,Java存放.class的目录或jar包也可以包含任意其他类型的文件,例如:

  • 配置文件,例如.properties;
  • 图片文件,例如.jpg;
  • 文本文件,例如.txt,.csv;
  • ……

从classpath读取文件就可以避免不同环境下文件路径不一致的问题:如果我们把default.properties文件放到classpath中,就不用关心它的实际存放路径。
在classpath中的资源文件,路径总是以/开头,我们先获取当前的Class对象,然后调用getResourceAsStream()就可以直接从classpath读取任意的资源文件:

1
2
3
try (InputStream input = getClass().getResourceAsStream("/default.properties")) {
// TODO:
}

调用getResourceAsStream()需要特别注意的一点是,如果资源文件不存在,它将返回null。因此,我们需要检查返回的InputStream是否为null,如果为null,表示资源文件在classpath中没有找到:
1
2
3
4
5
try (InputStream input = getClass().getResourceAsStream("/default.properties")) {
if (input != null) {
// TODO:
}
}

如果我们把默认的配置放到jar包中,再从外部文件系统读取一个可选的配置文件,就可以做到既有默认的配置文件,又可以让用户自己修改配置:
1
2
3
Properties props = new Properties();
props.load(inputStreamFromClassPath("/default.properties"));
props.load(inputStreamFromFile("./conf.properties"));

这样读取配置文件,应用程序启动就更加灵活。

小结

  • 把资源存储在classpath中可以避免文件路径依赖;
  • Class对象的getResourceAsStream()可以从classpath中读取指定资源;
  • 根据classpath读取资源时,需要检查返回的InputStream是否为null。

序列化

序列化是指把一个Java对象变成二进制内容,本质上就是一个byte[]数组。
为什么要把Java对象序列化呢?因为序列化后可以把byte[]保存到文件中,或者把byte[]通过网络传输到远程,这样,就相当于把Java对象存储到文件或者通过网络传输出去了。
有序列化,就有反序列化,即把一个二进制内容(也就是byte[]数组)变回Java对象。有了反序列化,保存到文件中的byte[]数组又可以“变回”Java对象,或者从网络上读取byte[]并把它“变回”Java对象。
我们来看看如何把一个Java对象序列化。
一个Java对象要能序列化,必须实现一个特殊的java.io.Serializable接口,它的定义如下:

1
2
public interface Serializable {
}

Serializable接口没有定义任何方法,它是一个空接口。我们把这样的空接口称为“标记接口”(Marker Interface),实现了标记接口的类仅仅是给自身贴了个“标记”,并没有增加任何方法。

序列化

把一个Java对象变为byte[]数组,需要使用ObjectOutputStream。它负责把一个Java对象写入一个字节流:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import java.io.*;
import java.util.Arrays;

public class Main {
public static void main(String[] args) throws IOException {
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
try (ObjectOutputStream output = new ObjectOutputStream(buffer)) {
// 写入int:
output.writeInt(12345);
// 写入String:
output.writeUTF("Hello");
// 写入Object:
output.writeObject(Double.valueOf(123.456));
}
System.out.println(Arrays.toString(buffer.toByteArray()));
}
}

ObjectOutputStream既可以写入基本类型,如int,boolean,也可以写入String(以UTF-8编码),还可以写入实现了Serializable接口的Object。
因为写入Object时需要大量的类型信息,所以写入的内容很大。

反序列化

和ObjectOutputStream相反,ObjectInputStream负责从一个字节流读取Java对象:

1
2
3
4
5
try (ObjectInputStream input = new ObjectInputStream(...)) {
int n = input.readInt();
String s = input.readUTF();
Double d = (Double) input.readObject();
}

除了能读取基本类型和String类型外,调用readObject()可以直接返回一个Object对象。要把它变成一个特定类型,必须强制转型。
readObject()可能抛出的异常有:

  • ClassNotFoundException:没有找到对应的Class;
  • InvalidClassException:Class不匹配。

对于ClassNotFoundException,这种情况常见于一台电脑上的Java程序把一个Java对象,例如,Person对象序列化以后,通过网络传给另一台电脑上的另一个Java程序,但是这台电脑的Java程序并没有定义Person类,所以无法反序列化。
对于InvalidClassException,这种情况常见于序列化的Person对象定义了一个int类型的age字段,但是反序列化时,Person类定义的age字段被改成了long类型,所以导致class不兼容。
为了避免这种class定义变动导致的不兼容,Java的序列化允许class定义一个特殊的serialVersionUID静态变量,用于标识Java类的序列化“版本”,通常可以由IDE自动生成。如果增加或修改了字段,可以改变serialVersionUID的值,这样就能自动阻止不匹配的class版本:

1
2
3
public class Person implements Serializable {
private static final long serialVersionUID = 2709425275741743919L;
}

要特别注意反序列化的几个重要特点:
反序列化时,由JVM直接构造出Java对象,不调用构造方法,构造方法内部的代码,在反序列化时根本不可能执行。

安全性

因为Java的序列化机制可以导致一个实例能直接从byte[]数组创建,而不经过构造方法,因此,它存在一定的安全隐患。一个精心构造的byte[]数组被反序列化后可以执行特定的Java代码,从而导致严重的安全漏洞。
实际上,Java本身提供的基于对象的序列化和反序列化机制既存在安全性问题,也存在兼容性问题。更好的序列化方法是通过JSON这样的通用数据结构来实现,只输出基本类型(包括String)的内容,而不存储任何与代码相关的信息。

小结

  • 可序列化的Java对象必须实现java.io.Serializable接口,类似Serializable这样的空接口被称为“标记接口”(Marker Interface);
  • 反序列化时不调用构造方法,可设置serialVersionUID作为版本号(非必需);
  • Java的序列化机制仅适用于Java,如果需要与其它语言交换数据,必须使用通用的序列化方法,例如JSON。

Reader

Reader是Java的IO库提供的另一个输入流接口。和InputStream的区别是,InputStream是一个字节流,即以byte为单位读取,而Reader是一个字符流,即以char为单位读取:

InputStream Reader
字节流,以byte为单位 字符流,以char为单位
读取字节(-1,0~255)int read() 读取字符(-1,0~65535)int read()
读到字节数组:int read(byte[] b) 读到字符数组:int read(char[] c)

java.io.Reader是所有字符输入流的超类,它最主要的方法是:

1
public int read() throws IOException;

这个方法读取字符流的下一个字符,并返回字符表示的int,范围是0~65535。如果已读到末尾,返回-1。

FileReader

FileReader是Reader的一个子类,它可以打开文件并获取Reader。下面的代码演示了如何完整地读取一个FileReader的所有字符:

1
2
3
4
5
6
7
8
9
10
11
12
public void readFile() throws IOException {
// 创建一个FileReader对象:
Reader reader = new FileReader("src/readme.txt"); // 字符编码是???
for (;;) {
int n = reader.read(); // 反复调用read()方法,直到返回-1
if (n == -1) {
break;
}
System.out.println((char)n); // 打印char
}
reader.close(); // 关闭流
}

如果我们读取一个纯ASCII编码的文本文件,上述代码工作是没有问题的。但如果文件中包含中文,就会出现乱码,因为FileReader默认的编码与系统相关,例如,Windows系统的默认编码可能是GBK,打开一个UTF-8编码的文本文件就会出现乱码。
要避免乱码问题,我们需要在创建FileReader时指定编码:
1
Reader reader = new FileReader("src/readme.txt", StandardCharsets.UTF_8);

和InputStream类似,Reader也是一种资源,需要保证出错的时候也能正确关闭,所以我们需要用try (resource)来保证Reader在无论有没有IO错误的时候都能够正确地关闭:
1
2
3
try (Reader reader = new FileReader("src/readme.txt", StandardCharsets.UTF_8) {
// TODO
}

Reader还提供了一次性读取若干字符并填充到char[]数组的方法:
1
public int read(char[] c) throws IOException

它返回实际读入的字符个数,最大不超过char[]数组的长度。返回-1表示流结束。
利用这个方法,我们可以先设置一个缓冲区,然后,每次尽可能地填充缓冲区:
1
2
3
4
5
6
7
8
9
public void readFile() throws IOException {
try (Reader reader = new FileReader("src/readme.txt", StandardCharsets.UTF_8)) {
char[] buffer = new char[1000];
int n;
while ((n = reader.read(buffer)) != -1) {
System.out.println("read " + n + " chars.");
}
}
}

CharArrayReader

CharArrayReader可以在内存中模拟一个Reader,它的作用实际上是把一个char[]数组变成一个Reader,这和ByteArrayInputStream非常类似:

1
2
try (Reader reader = new CharArrayReader("Hello".toCharArray())) {
}

StringReader

StringReader可以直接把String作为数据源,它和CharArrayReader几乎一样:

1
2
try (Reader reader = new StringReader("Hello")) {
}

InputStreamReader

Reader和InputStream有什么关系?
除了特殊的CharArrayReader和StringReader,普通的Reader实际上是基于InputStream构造的,因为Reader需要从InputStream中读入字节流(byte),然后,根据编码设置,再转换为char就可以实现字符流。如果我们查看FileReader的源码,它在内部实际上持有一个FileInputStream。
既然Reader本质上是一个基于InputStream的byte到char的转换器,那么,如果我们已经有一个InputStream,想把它转换为Reader,是完全可行的。InputStreamReader就是这样一个转换器,它可以把任何InputStream转换为Reader。示例代码如下:

1
2
3
4
// 持有InputStream:
InputStream input = new FileInputStream("src/readme.txt");
// 变换为Reader:
Reader reader = new InputStreamReader(input, "UTF-8");

构造InputStreamReader时,我们需要传入InputStream,还需要指定编码,就可以得到一个Reader对象。上述代码可以通过try (resource)更简洁地改写如下:
1
2
3
try (Reader reader = new InputStreamReader(new FileInputStream("src/readme.txt"), "UTF-8")) {
// TODO:
}

上述代码实际上就是FileReader的一种实现方式。
使用try (resource)结构时,当我们关闭Reader时,它会在内部自动调用InputStream的close()方法,所以,只需要关闭最外层的Reader对象即可。

使用InputStreamReader,可以把一个InputStream转换成一个Reader。

小结

  • Reader定义了所有字符输入流的超类:
    • FileReader实现了文件字符流输入,使用时需要指定编码;
    • CharArrayReader和StringReader可以在内存中模拟一个字符流输入。
  • Reader是基于InputStream构造的:可以通过InputStreamReader在指定编码的同时将任何InputStream转换为Reader。
  • 总是使用try (resource)保证Reader正确关闭。

Writer

Reader是带编码转换器的InputStream,它把byte转换为char,而Writer就是带编码转换器的OutputStream,它把char转换为byte并输出。
Writer和OutputStream的区别如下:

OutputStream Writer
字节流,以byte为单位 字符流,以char为单位
写入字节(0~255):void write(int b) 写入字符(0~65535):void write(int c)
写入字节数组:void write(byte[] b) 写入字符数组:void write(char[] c)
无对应方法 写入String:void write(String s)

Writer是所有字符输出流的超类,它提供的方法主要有:

  • 写入一个字符(0~65535):void write(int c);
  • 写入字符数组的所有字符:void write(char[] c);
  • 写入String表示的所有字符:void write(String s)。

FileWriter

FileWriter就是向文件中写入字符流的Writer。它的使用方法和FileReader类似:

1
2
3
4
5
try (Writer writer = new FileWriter("readme.txt", StandardCharsets.UTF_8)) {
writer.write('H'); // 写入单个字符
writer.write("Hello".toCharArray()); // 写入char[]
writer.write("Hello"); // 写入String
}

CharArrayWriter

CharArrayWriter可以在内存中创建一个Writer,它的作用实际上是构造一个缓冲区,可以写入char,最后得到写入的char[]数组,这和ByteArrayOutputStream非常类似:

1
2
3
4
5
6
try (CharArrayWriter writer = new CharArrayWriter()) {
writer.write(65);
writer.write(66);
writer.write(67);
char[] data = writer.toCharArray(); // { 'A', 'B', 'C' }
}

StringWriter

StringWriter也是一个基于内存的Writer,它和CharArrayWriter类似。实际上,StringWriter在内部维护了一个StringBuffer,并对外提供了Writer接口。

OutputStreamWriter

除了CharArrayWriter和StringWriter外,普通的Writer实际上是基于OutputStream构造的,它接收char,然后在内部自动转换成一个或多个byte,并写入OutputStream。因此,OutputStreamWriter就是一个将任意的OutputStream转换为Writer的转换器:

1
2
3
try (Writer writer = new OutputStreamWriter(new FileOutputStream("readme.txt"), "UTF-8")) {
// TODO:
}

上述代码实际上就是FileWriter的一种实现方式。这和上一节的InputStreamReader是一样的。

小结

  • Writer定义了所有字符输出流的超类:
    • FileWriter实现了文件字符流输出;
    • CharArrayWriter和StringWriter在内存中模拟一个字符流输出。
  • 使用try (resource)保证Writer正确关闭。
  • Writer是基于OutputStream构造的,可以通过OutputStreamWriter将OutputStream转换为Writer,转换时需要指定编码。

PrintStream和PrintWriter

PrintStream

PrintStream是一种FilterOutputStream,它在OutputStream的接口上,额外提供了一些写入各种数据类型的方法:

  • 写入int:print(int)
  • 写入boolean:print(boolean)
  • 写入String:print(String)
  • 写入Object:print(Object),实际上相当于print(object.toString())

以及对应的一组println()方法,它会自动加上换行符。
我们经常使用的System.out.println()实际上就是使用PrintStream打印各种数据。其中,System.out是系统默认提供的PrintStream,表示标准输出:

1
2
3
System.out.print(12345); // 输出12345
System.out.print(new Object()); // 输出类似java.lang.Object@3c7a835a
System.out.println("Hello"); // 输出Hello并换行

System.err是系统默认提供的标准错误输出。
PrintStream和OutputStream相比,除了添加了一组print()/println()方法,可以打印各种数据类型,比较方便外,它还有一个额外的优点,就是不会抛出IOException,这样我们在编写代码的时候,就不必捕获IOException。

PrintWriter

PrintStream最终输出的总是byte数据,而PrintWriter则是扩展了Writer接口,它的print()/println()方法最终输出的是char数据。两者的使用方法几乎是一模一样的:

1
2
3
4
5
6
7
8
9
10
11
12
import java.io.*;
public class Main {
public static void main(String[] args) {
StringWriter buffer = new StringWriter();
try (PrintWriter pw = new PrintWriter(buffer)) {
pw.println("Hello");
pw.println(12345);
pw.println(true);
}
System.out.println(buffer.toString());
}
}

小结

  • PrintStream是一种能接收各种数据类型的输出,打印数据时比较方便:
    • System.out是标准输出;
    • System.err是标准错误输出。
  • PrintWriter是基于Writer的输出。

使用Files

从Java 7开始,提供了Files和Paths这两个工具类,能极大地方便我们读写文件。
虽然Files和Paths是java.nio包里面的类,但他俩封装了很多读写文件的简单方法,例如,我们要把一个文件的全部内容读取为一个byte[],可以这么写:

1
byte[] data = Files.readAllBytes(Paths.get("/path/to/file.txt"));

如果是文本文件,可以把一个文件的全部内容读取为String:
1
2
3
4
5
6
// 默认使用UTF-8编码读取:
String content1 = Files.readString(Paths.get("/path/to/file.txt"));
// 可指定编码:
String content2 = Files.readString(Paths.get("/path/to/file.txt"), StandardCharsets.ISO_8859_1);
// 按行读取并返回每行内容:
List<String> lines = Files.readAllLines(Paths.get("/path/to/file.txt"));

写入文件也非常方便:
1
2
3
4
5
6
7
8
// 写入二进制文件:
byte[] data = ...
Files.write(Paths.get("/path/to/file.txt"), data);
// 写入文本并指定编码:
Files.writeString(Paths.get("/path/to/file.txt"), "文本内容...", StandardCharsets.ISO_8859_1);
// 按行写入文本:
List<String> lines = ...
Files.write(Paths.get("/path/to/file.txt"), lines);

此外,Files工具类还有copy()、delete()、exists()、move()等快捷方法操作文件和目录。
最后需要特别注意的是,Files提供的读写方法,受内存限制,只能读写小文件,例如配置文件等,不可一次读入几个G的大文件。读写大型文件仍然要使用文件流,每次只读写一部分文件内容。

小结

  • 对于简单的小文件读写操作,可以使用Files工具类简化代码。

length(), length, size()

  • length() 方法是针对字符串来说的,要求一个字符串的长度就要用到它的length()方法;
  • length 属性是针对 Java 中的数组来说的,要求数组的长度可以用其 length 属性;
  • size() 方法是针对泛型集合说的, 如果想看这个泛型有多少个元素, 就调用此方法来查看!

例子

1
2
import java.util.ArrayList;
import java.util.List;

public class Main {

  public static void main(String[] args) {
     String array[] = { "First", "Second", "Third" };
     String a = "HelloWorld";
     List<String> list = new ArrayList<String>();
     list.add(a);
     System.out.println("数组array的长度为" + array.length);
     System.out.println("字符串a的长度为" + a.length());
     System.out.println("list中元素个数为" + list.size());

  }

}

输出

数组array的长度为3
字符串a的长度为10
list中元素个数为1

0%