IO流基础
IO流
File,IO流概述,File文件的创建
如果想要长久的保存文件,我们需要进行文件的存储
文件是非常重要的存储方式,在计算机硬盘中。
即便断电,或者程序终止了,存储在硬盘文件中的数据也不会丢失。
File是java.io.包下的类, File类的对象,用于代表当前操作系统的文件(可以是文件、或文件夹)。
- 获取文件信息(大小,文件名,修改时间)
- 判断文件的类型
- 创建文件/文件夹
- 删除文件/文件夹
注意:File类只能对文件本身进行操作,不能读写文件里面存储的数据。
就是我们利用File来进行操作文件,如果想要读写我们要使用 IO流
创建File对象
构造器 | 说明 |
---|---|
public File(String pathname) | 根据文件路径创建文件对象 |
public File(String parent, String child) | 根据父路径和子路径名字创建文件对象 |
public File(File parent, String child) | 根据父路径对应文件对象和子路径名字创建文件对象 |
注意
File对象既可以代表文件、也可以代表文件夹。
File封装的对象仅仅是一个路径名,这个路径可以是存在的,也允许是不存在的。
File file = new File("D:\\code\\博客\\temp\\类别1.txt");
System.out.println(file.length());
绝对路径
File file1 = new File(“D:\\itheima\\a.txt”);
相对路径
File file3 = new File(“模块名\\a.txt”);
常用使用方法
方法名称 | 说明 |
---|---|
public boolean exists() | 判断当前文件对象,对应的文件路径是否存在,存在返回true |
public boolean isFile() | 判断当前文件对象指代的是否是文件,是文件返回true,反之。 |
public boolean isDirectory() | 判断当前文件对象指代的是否是文件夹,是文件夹返回true,反之。 |
public String getName() | 获取文件的名称(包含后缀) |
public long length() | 获取文件的大小,返回字节个数 |
public long lastModified() | 获取文件的最后修改时间。 |
public String getPath() | 获取创建文件对象时,使用的路径 |
public String getAbsolutePath() | 获取绝对路径 |
File file = new File("D:\\code\\javaidea\\summary_study\\src\\File\\abc.txt");
System.out.println(file.exists()); //true
System.out.println(file.isFile()); //true
System.out.println(file.isDirectory()); //false
System.out.println(file.getName()); //abc
System.out.println(file.length());
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
long l = file.lastModified();
System.out.println(sdf.format(l));
System.out.println(file.getPath());
System.out.println(file.getAbsolutePath());
创建文件,删除文件
File类创建文件的功能
方法名称 | 说明 |
---|---|
public boolean createNewFile() | 创建一个新的空的文件 |
public boolean mkdir() | 只能创建一级文件夹 |
public boolean mkdirs() | 可以创建多级文件夹 |
File f = new File("D:\\code\\博客\\temp\\create.txt");
System.out.println(f.createNewFile());
File f1 = new File("D:\\code\\博客\\temp\\aa\\bb\\cc");
System.out.println(f1.mkdirs());
System.out.println(f.delete());
System.out.println(f1.delete());
File类删除文件的功能
方法名称 | 说明 |
---|---|
public boolean delete() | 删除文件、空文件夹 |
注意:delete方法默认只能删除文件和空文件夹,删除后的文件不会进入回收站。
遍历文件夹
File类提供的遍历文件夹的功能
方法名称 | 说明 |
---|---|
public String[] list() | 获取当前目录下所有的"一级文件名称"到一个字符串数组中去返回。 |
public File[] listFiles() | 获取当前目录下所有的"一级文件对象"到一个文件对象数组中去返回(重点) |
使用listFiles方法时的注意事项:
- 当主调是文件,或者路径不存在时,返回null listFiles只能针对的是文件夹对象
- 当主调是空文件夹时,返回一个长度为0的数组
- 当主调是一个有内容的文件夹时,将里面所有一级文件和文件夹的路径放在File数组中返回
- 当主调是一个文件夹,且里面有隐藏文件时,将里面所有文件和文件夹的路径放在File数组中返回,包含隐藏文件
- 当主调是一个文件夹,但是没有权限访问该文件夹时,返回null 管理员权限
对于这两种遍历方式,我们一般用第二种比较多,毕竟我们单纯获得名字没有什么作用.
方法递归
什么是方法递归?
递归是一种算法,在程序设计语言中广泛应用。
从形式上说:方法调用自身的形式称为方法递归( recursion)。
递归的形式
- 直接递归:方法自己调用自己。
- 间接递归:方法调用其他方法,其他方法又回调方法自己。
使用方法递归时需要注意的问题:
递归如果没有控制好终止,会出现递归死循环,导致栈内存溢出错误。,因为方法是存在 与栈里面的,所以如果出现死循环就会出现这个栈内存的溢出
递归的简单demo
需求:计算n的阶乘,5的阶乘=12345; 6的阶乘=123456;
分析
假如我们认为存在一个公式是 f(n) = 1234567…(n-1)*n;
那么公式等价形式就是: f(n) = f(n-1) *n
如果求的是 1-5的阶乘 的结果,我们手工应该应该如何应用上述公式计算。
f(5) = f(4) * 5
f(4) = f(3) * 4
f(3) = f(2) * 3
f(2) = f(1) * 2
f(1) = 1
public static void main(String[] args) {
System.out.println(fn(8));
}
public static int fn(int num){
if(num>1){
return num*fn(num-1);
}else {
return 1;
}
}
经典的猴子摘桃问题
猴子第一天摘下若干桃子,当即吃了一半,觉得好不过瘾,于是又多吃了一个第二天又吃了前天剩余桃子数量的一半,觉得好不过瘾,于是又多吃了一个以后每天都是吃前天剩余桃子数量的一半,觉得好不过瘾,又多吃了一个等到第10天的时候发现桃子只有1个了。
需求:请问猴子第一天摘了多少个桃子?
分析:
整体来看,每一天都是做同一个事件,典型的规律化问题,考虑递归三要素:
递归公式:
递归终结点:
递归方向:
public static void main(String[] args) {
System.out.println(eat(1));
}
public static int eat(int num){
if(num==10){
return 1;
}else {
return 2*(eat(num+1)+1);
}
}
文件搜索
注意点
- 我们要考虑这个文件遍历的多种情况
当输入的文件埃及,没有权限,不存在,是个文件怎么办
//文件名不满足格式的集中情况
if(dir==null||!dir.exists()|| dir.isFile()){
System.out.println("未找到");
return;
}
- 对于一个文件夹对象我们要考虑
文件夹不为空,获取的长度要大于0
if (files!=null&&files.length>0)
package file.Recursion;
import java.io.File;
import java.io.IOException;
public class SearchFile {
public static void main(String[] args) throws Exception {
searchFile(new File("D:/code"),"类别1");
}
/**
*去目录下去搜索文件
* dir目录
* name要搜索的文件名字
*/
public static void searchFile(File dir, String name) throws Exception {
//文件名不满足格式的集中情况
if(dir==null||!dir.exists()|| dir.isFile()){
System.out.println("未找到");
return;
}
File[] files = dir.listFiles();
if (files!=null&&files.length>0) {
for (File file : files) {
//如果是一个文件
if(file.isFile()){
//System.out.println(file.getAbsolutePath());
//System.out.println(file.getName());
if ( file.getName().contains(name)) {
//System.out.println("111");
String run = "cmd /c start " + file.getAbsoluteFile();
System.out.println(run);
Runtime.getRuntime().exec(run);
System.out.println(file.getAbsoluteFile());
}
}else {
searchFile(file,name);
}
}
}
}
}
删除空文件夹
package file.Recursion;
import java.io.File;
public class DeleteNull {
public static void main(String[] args) {
deleteFile(new File("D:/test"));
}
public static void deleteFile(File dir){
// dir==null 就是无法获取内容
if(dir==null||!dir.exists()){
System.out.println("未找到");
return;
}
if(dir.isFile()){
dir.delete();
return;
}
File[] files = dir.listFiles();
if(files==null){
return;
}
if(files.length==0){
dir.delete();
return;
}
for (File file : files) {
if(file.isFile()){
System.out.println(file.getName());
file.delete();//把自己删除了
}else {
System.out.println(file.getName());
deleteFile(file);
file.delete();
}
}
dir.delete();
}
}
啤酒问题
需求:
啤酒2元1瓶,4个盖子可以换一瓶,2个空瓶可以换一瓶,
请问10元钱可以喝多少瓶酒,剩余多少空瓶和盖子。
答案:
15瓶 3盖子 1瓶子
package file.Recursion;
public class Drink {
public static int totalNumber=0; //默认为0
public static int lastBottleNumber ;
public static int lastCoverNumber;
public static void main(String[] args) {
//啤酒问题,啤酒两元一瓶,4个盒子可以换一瓶,两个空瓶可以换一瓶,请问10元可以换几瓶buy
buy(10);
System.out.println(totalNumber);
System.out.println(lastBottleNumber);
System.out.println(lastCoverNumber);
}
public static void buy(int money){
int buyNumber = money/2;
totalNumber+=buyNumber;
//现在剩了buyNumber个瓶盖和瓶子
int bottleNumber = buyNumber;
int coverNumber = buyNumber;
//两者不同时为0
//并不是两者,而是上一轮循环
while (buyNumber!=0){
buyNumber=0;//每一轮的新增数量
buyNumber+=bottleNumber/2;
buyNumber+=coverNumber/4;
bottleNumber=bottleNumber%2+buyNumber;
coverNumber=coverNumber%4+buyNumber;
totalNumber+=buyNumber;
}
lastBottleNumber=bottleNumber;
lastCoverNumber= coverNumber;
}
}
字符
Ascii
标准ASCII字符集
ASCII(American Standard Code for Information Interchange): 美国信息交换标准代码,包括了英文、符号等。
标准ASCII使用1个字节存储一个字符,首尾是0,总共可表示128个字符,对美国佬来说完全够用。
GBK
GBK(汉字内码扩展规范,国标)
汉字编码字符集,包含了2万多个汉字等字符,GBK中一个中文字符编码成两个字节的形式存储。
注意:GBK兼容了ASCII字符集。
中文是两个字节一个字符
比如说我a你,那么Ascii就是用0开头的 ,但是这个中文就是这个1开头的
Unicode
Unicode字符集(统一码,也叫万国码)
Unicode是国际组织制定的,可以容纳世界上所有文字、符号的字符集。
UTF-8
是Unicode字符集的一种编码方案,采取可变长编码方案,共分四个长度区:1个字节,2个字节,3个字节,4个字节
英文字符、数字等只占1个字节(兼容标准ASCII编码),汉字字符占用3个字节
0xxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx 0xxxxxxx
a 97 01100001
我 25105 110 001000 010001
m 109 01101101
01100001 1110xxxx 10xxxxxxx 10xxxxxx 01101101
就是将中文的字节分为几份,然后将这个字符放到划分好的位置里
三个字节的开头是固定的1110 中文字节 10中文字节 10中文字节
UTF-8编码方式(二进制) |
---|
0xxxxxxx (ASCII码) |
110xxxxx 10xxxxxx |
1110xxxx 10xxxxxx 10xxxxxx |
11110xxx 10xxxxxx 10xxxxxx 10xxxxxx |
注意:技术人员在开发时都应该使用UTF-8编码!
总结
ASCII字符集:只有英文、数字、符号等,占1个字节。
GBK字符集:汉字占2个字节,英文、数字占1个字节。
UTF-8字符集:汉字占3个字节,英文、数字占1个字节。
注意1:字符编码时使用的字符集,和解码时使用的字符集必须一致,否则会出现乱码
注意2:英文,数字一般不会乱码,因为很多字符集都兼容了ASCII编码。
java代码完成对字符的编码
Java代码完成对字符的编码
String提供了如下方法 | 说明 |
---|---|
byte[] getBytes() | 使用平台的默认字符集将该 String编码为一系列字节,将结果存储到新的字节数组中 |
byte[] getBytes(String charsetName) | 使用指定的字符集将该 String编码为一系列字节,将结果存储到新的字节数组中 |
String data = "我爱你1314";
byte[] bytes = data.getBytes(); //默认是按照平台默认的字符集来编码的UTF-8
System.out.println(Arrays.toString(bytes));
//[-26, -120, -111, -25, -120, -79, -28, -67, -96, 49, 51, 49, 52]
//中文又三个,英文喝数字是1个,中文是负数是因为开头是1
byte[] bytes1 = data.getBytes("GBK"); //会出现异常,主要是IDEA担心你书写错误,如果确认你的代码正确,我们就可以把这个错误抛出
System.out.println(Arrays.toString(bytes1));
//[-50, -46, -80, -82, -60, -29, 49, 51, 49, 52]
Java代码完成对字符的解码
String提供了如下方法 | 说明 |
---|---|
String(byte[] bytes) | 通过使用平台的默认字符集解码指定的字节数组来构造新的 String |
String(byte[] bytes, String charsetName) | 通过指定的字符集解码指定的字节数组来构造新的 String |
byte[] bytes,就是你之前获得这个字符的编码集
IO流
I指Input,称为输入流:负责把数据读到内存中去
O指Output,称为输出流:负责写数据出去
怎么学习IO流呢
1、先搞清楚IO流的分类、体系。
2、再挨个学习每个IO流的作用、用法。
IO流的分类
IO流总体来看就是四大流**,字节输入流,字节输出流,字符输入流,字符输出流**
- 字节输入流:以内存为基准,来自磁盘文件/网络中的数据以字节的形式读入到内存中去的流
- 字节输出流:以内存为基准,把内存中的数据以字节写出到磁盘文件或者网络中去的流。
- 字符输入流:以内存为基准,来自磁盘文件/网络中的数据以字符的形式读入到内存中去的流。
- 字符输出流:以内存为基准,把内存中的数据以字符写出到磁盘文件或者网络介质中去的流。
IO流是怎么划分的,大体分为几类,各自的作用?
字节输入流 InputStream(读字节数据的)
字节输出流 OutoutStream(写字节数据出去的)
字符输入流 Reader(读字符数据的)
字符输出流 Writer(写字符数据出去的)
IO流的体系
FileInputStream(文件字节输入流)
作用:以内存为基准,可以把磁盘文件中的数据以字节的形式读入到内存中去。
构造器 | 说明 |
---|---|
public FileInputStream(File file) | 创建字节输入流管道与源文件接通 |
public FileInputStream(String pathname) | 创建字节输入流管道与源文件接通 |
方法名称 | 说明 |
---|---|
public int read( | 每次读取一个字节返回,如果发现没有数据可读会返回-1. |
public int read(byte[] buffer) | 每次用一个字节数组去读取数据,返回字节数组读取了多少个字节,如果发现没有数据可读会返回-1. |
read读取的数据是返回的
使用FileInputStream每次读取一个字节,读取性能较差,并且读取汉字输出会乱码。
read读取文件
InputStream is = new FileInputStream("D:\\code\\javaidea\\summary_study\\src\\file\\fileStream\\test.txt" int b1 = is.read();
System.out.println(b1);
int b2 = is.read();
System.out.println(b2);
int b3 = is.read();
System.out.println(b3);
优化代码
InputStream is = new FileInputStream("D:\\code\\javaidea\\summary_study\\src\\file\\fileStream\\test.txt"
int b ;
while ((b=is.read())!=-1){
System.out.print((char) b);
}
1、使用字节流读取中文,如何保证输出不乱码,怎么解决?
定义一个与文件一样大的字节数组,一次性读取完文件的全部字节
文件字节输入流:一次读取完全部字节
方式一:自己定义一个字节数组与被读取的文件大小一样大,然后使用该字节数组,一次读完文件的全部字节。
方法名称 | 说明 |
---|---|
public int read(byte[] buffer) | 每次用一个字节数组去读取,返回字节数组读取了多少个字节,如果发现没有数据可读会返回-1. |
我们要注意这个读取的内容存放到这个byte数组中了
这个也会对汉字的读取造成这个乱码,因为可能在读取的时候我们可能会将中文字节给截断
InputStream is = new FileInputStream("D:\\code\\javaidea\\summary_study\\src\\file\\fileStream\\test.txt");
byte[] bytes = new byte[3];
int len = is.read(bytes);
String s = new String(bytes);
System.out.println(s);
System.out.println("length"+len);
int len2 = is.read(bytes);
String s2 = new String(bytes,0,len2);
System.out.println(s2);
System.out.println("length"+len2);
我们读取的时候注意用到了这个String s2 = new String(bytes,0,len2);
这是因为我们在使用这个read读取的过程中,有可能出现这个第二次读取的内容比第一次少(文本的数量不够,第二次剩余的字节不足以填满这个字节数组)
所以我们在处理的时候我们可以通过取了多少,就倒出来多少的方式来实现这个
len2就是第二次读取内容的时候的读取的字节的长度
循环改造
byte[] bytes = new byte[3];
int len;
while ((len=is.read(bytes))!=-1){
String rs = new String(bytes,0,len);
System.out.println(rs);
}
is.close();
由上述代码可以知道,我们如果想要不输出中文乱码的情况下的话我们就需要定义一个与文件一样大的字节数组,一次性读取文件的全部字节,保证字节读取的时候中文字符不被截取
方法名称 | 说明 |
---|---|
public byte[] readAllBytes() throws IOException | 直接将当前字节输入流对应的文件对象的字节数据装到一个字节数组返回 |
byte[] buffer = is.readAllBytes();
System.out.println(new String(buffer)
FileOutputStream(文件字节输出流)
作用:以内存为基准,把内存中的数据以字节的形式写出到文件中去
构造器
构造器 | 说明 |
---|---|
public FileOutputStream(File file) | 创建字节输出流管道与源文件对象接通 |
public FileOutputStream(String filepath) | 创建字节输出流管道与源文件路径接通 |
public FileOutputStream(File file,boolean append) | 创建字节输出流管道与源文件对象接通,可追加数据 |
public FileOutputStream(String filepath,boolean append) | 创建字节输出流管道与源文件路径接通,可追加数据 |
OutputStream os = new FileOutputStream("D:\\code\\javaidea\\summary_study\\src\\file\\fileStream\\output.txt");
public FileOutputStream(File file,boolean append)
这个追加就是我们不用每次写入数据的时候不用覆盖原本的数据(会把原来的清空)
OutputStream os = *new* FileOutputStream("D:\\code\\javaidea\\summary_study\\src\\file\\fileStream\\output.txt",*true*);
换行
\r\n,这样支持的平台更多,不仅可以在windows平台换行,也可以在linux平台换行
byte[] bytes1 = "\r\n".getBytes();
os.write(bytes1);
方法
方法名称 | 说明 |
---|---|
public void write(int a) | 写一个字节出去 |
public void write(byte[] buffer) | 写一个字节数组出去 |
public void write(byte[] buffer , int pos , int len) | 写一个字节数组的一部分出去。 |
public void close() throws IOException | 关闭流。 |
os.write(97);
os.write('b');
os.write('豪'); //它一次只能写一个字节,汉字是三个字节,所以我们不能将这个中文写入文件中
byte[] bytes = "我爱java".getBytes();
os.write(bytes);
os.close();
文件复制
public static void main(String[] args) throws Exception {
InputStream in = new FileInputStream("D:\\code\\javaidea\\summary_study\\src\\file\\fileStream\\output.txt");
byte[] bytes = in.readAllBytes();
OutputStream os = new FileOutputStream("D:\\code\\javaidea\\summary_study\\src\\file\\fileStream\\temp.txt");
System.out.println(Arrays.toString(bytes));
os.write(bytes);
in.close();
os.close();
}
我写的代码的缺陷,我用的是这个readAllBytes,因为我知道这个文件的大小不大,但是真正当我们自己去复制的时候,有可能会遇到很大的文件,这个时候我们在使用这个readAllBytes函数就会出现异常了
注意事项
流使用完毕之后一定要注意关闭这个流
用close方法来进行关闭
read就像是链表的指针一样(类比),每次执行的时候它自己就会自动的往后移
使用字节流读取中文,如何保证输出不乱码,怎么解决?
定义一个与文件一样大的字节数组,一次性读取完文件的全部字节。保证字节读取的时候文件不是乱码
java9之后新增了一个函数 readAllBytes(),这个函数可以读取一个文件中的所有字节,注意这个是java9之后才有的
字节流更适合进行文件的复制
释放资源
原来用close释放文件的方式是不合理的
万一中间出现了异常,那么写在最后的流就没有机会释放内存了
- try-catch-finally
- try-with-resource
try-catch-finally
finally代码区的特点:无论try中的程序是正常执行了,还是出现了异常,最后都一定会执行finally区,除非JVM终止----(System.exit(0))
try {
System.out.println(10/0);
return;
}catch (Exception e){
e.printStackTrace();
}finally {
System.out.println("woaini");
}
注意点
不要在finally里中返回数据,这样的话,你在前面返回的数据就失效了
finally一般用于在程序执行完毕之后进行资源的释放操作
由于这个代码中又有catch代码,我们又可以捕获异常,又可以释放空间,一举两得
InputStream in=null;
OutputStream os=null;
try {
in = new FileInputStream("D:\\code\\javaidea\\summary_study\\src\\file\\fileStream\\output.txt");
byte[] bytes = in.readAllBytes();
os = new FileOutputStream("D:\\code\\javaidea\\summary_study\\src\\file\\fileStream\\temp.txt");
System.out.println(Arrays.toString(bytes));
os.write(bytes);
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
try {
if (in!=null) {
in.close();
}
if(os!=null){
os.close();
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
代码解析难点
- 关闭代码的时候为什么要先判断
因为我们在创建完这个文件对象之前FileInputStream("D:\\code\\javaidea\\summary_study\\src\\file\\fileStream\\output.txt");
有可能会有异常,比如说我输出了System.**out**.println(10/0);
这样的话,就会捕获异常,后面的初始化操作并没有执行,因此我们关闭的是个空指针,所以我们要在这个关闭的时候进行一个判断
- 为什么文件流的对象要放到这个try外面
因为我们的finaly中要对try中的变量进行操作,我们没有办法操作,所以只能见这个变量定义到这个try外面
if (in!=null) {
in.close();
}
try-with-resource
try catch finally 写代码的缺点就是太臃肿了,不优雅,JDK之后新出了一种解决方案
try-with-resource
格式
try(定义资源1;定义资源2;…){
可能出现异常的代码;
}catch(异常类名 变量名){
异常的处理代码;
}
注意点
() 中只能放置资源,否则报错
什么是资源呢?
资源一般指的是最终实现了AutoCloseable接口。(实现了自动关闭接口)
我们查看源码发现InputStream继承了Closeable接口,Closeable接口继承了AutoCloseable接口
该资源使用完毕后,会自动调用其close()方法,完成对资源的释放!
根据这种接口我们可以自己自定义这个资源对象
try(InputStream in = new FileInputStream("D:\\code\\javaidea\\summary_study\\src\\file\\fileStream\\output.txt");
OutputStream os = new FileOutputStream("D:\\code\\javaidea\\summary_study\\src\\file\\fileStream\\temp.txt");)
{
byte[] bytes = in.readAllBytes();
System.out.println(Arrays.toString(bytes));
os.write(bytes);
} catch (IOException e) {
throw new RuntimeException(e);
}
1691455739210)]
格式
try(定义资源1;定义资源2;…){
可能出现异常的代码;
}catch(异常类名 变量名){
异常的处理代码;
}
注意点
() 中只能放置资源,否则报错
什么是资源呢?
资源一般指的是最终实现了AutoCloseable接口。(实现了自动关闭接口)
我们查看源码发现InputStream继承了Closeable接口,Closeable接口继承了AutoCloseable接口
该资源使用完毕后,会自动调用其close()方法,完成对资源的释放!
根据这种接口我们可以自己自定义这个资源对象
try(InputStream in = new FileInputStream("D:\\code\\javaidea\\summary_study\\src\\file\\fileStream\\output.txt");
OutputStream os = new FileOutputStream("D:\\code\\javaidea\\summary_study\\src\\file\\fileStream\\temp.txt");)
{
byte[] bytes = in.readAllBytes();
System.out.println(Arrays.toString(bytes));
os.write(bytes);
} catch (IOException e) {
throw new RuntimeException(e);
}
本文作者:TranquilGlow
本文链接:https://www.cnblogs.com/xuehaiqiule/p/17621620.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步