目录
IDEA快捷键
Ctrl+Shift+F10 运行程序
Fn+Home 光标移动到行首
Fn+End 光标移动到行尾
Ctrl+Alt+V 自动补全变量
Ctrl+D 向下复制一行
Ctrl+F12 在类中查找方法
Shift 连按2下 查找类
Ctrl+F 查找输入字符串出现位置
Ctrl+Y 删除光标所在的一行
Alt+鼠标向下拖 编辑多行
Ctrl+P 查看方法参数
JDK JRE JVM三者之间的关系之间的关系
JDK是Java开发工具箱,JRE是Java运行环境,JVM是Java虚拟机。
JDK包括JRE, JRE包括JVM。
JDk是Java的development kit,Java开发工具箱。
JVM是不能独立安装的,但是JRE和JDK都是可以独立安装。
安装JDK的时候, JRE就自动安装了。同时,JRE内部的JVM也自动安装了。
问题
假设你在软件公司开发了一个新的软件。
现在要去客户那边要去客户那边给客户把项目部署一下,需要安装jdk吗?
假如说我现在在北京,我开发了一个软件,给武汉部署。这个时候呢,只需要安装JRE就行了。
JRE体积很小,安装非常便捷
你又不在这个武汉的这个客户机器上开发。
所以你不需要安装JDK
为什么安装JDK的时候会自带一个JRE
因为Java程序员开发完程序之后要测试这个程序,让这个程序运行起来。需要JRE。
----------------------------------------
基础
Windows操作系统需要安装windows版本的Java虚拟机,Linux操作系统上需要安装Linux版本的java虚拟机。
Java虚拟机是不一样的。
操作系统是不一样的。
但是你的JAVA程序是一样的。
你的Java程序是放在java虚拟机。
所以你的java程序压根儿就没有和windows是打交道。
JAVA虚拟机他把操作系统之间的差异给屏蔽掉了。
优点就是咱们跨平台了,对吧?编写一次。
但是缺点是麻烦,就是你你这个程序直接扔到windows里面能运行吗?
运行不了你的这个程序直接扔到Linux里面能运行吗?
运行不了。
你必须得有JVM。
而你要想有jvm,你必须得有jdk。
编译阶段和运行阶段可以在两个不同操作系统嘛?
可以,如windows编译生成的字节码放到linux运行。
字节码是二进制嘛?
不是,如果是就不需要jvm了,操作系统可以直接运行二进制
java命令后面只能跟类名
java HelloWorld
java HelloWorld.class(错误)
类加载器classloader默认从当前目录下找字节码文件。
classpath给类加载器指路,classpath不属于windows操作系统,属于java
变量
变量必须先声明再赋值,才能访问。
int age;
System.out.println(age);
报错未初始化变量age
int a,b,c=100;
a和b没有初始化赋值,只有c=100
----------------------------------------
取值范围
byte -128-127
最大可表示01111111(最左边符号位)
2^7-1
10000000 -1
整形数
数据默认当做int类型,需要存为long在后面加l或L
从小到大自动类型转换
long a =100;
long b=2147483648;(报错)
int最大2147483647
强制类型转换
long a =100L;
int b=(int)a; 因为100在int范围内,精度不会丢失
long类型强转int,会将前4个字节砍掉
short byte char混合运算
各自转换成int再运算
char c1='a';
byte b=1;
System.out.println(c1+b); //98
//short s=98 编译通过
//short s=c1+b;编译报错,因为加法运算编译阶段不知道结果,只知道结果类型为int
//short s=(short)c1+b 编译报错,运算顺序为short类型的c1+byte类型的b,结果还是int
short s=(short)(c1+b); //编译通过
多种数据类型混合运算,结果为范围最大的
long a =10L;
char c='a';
short s=100;
int i=30;
long x=a+c+s+i;
浮点型
银行/财务系统采用比double精度更高的java.math.Decimal(引用数据类型)
任意一个浮点型表示范围都大于整数型
float容量>long容量
浮点型默认当做double,加f或F表示float
switch语句
1.低版本JDK只支持int,(包括byte short char,存在自动类型转换)。高版本支持String int 枚举
2.如果分支执行,但是分支最后没有break,会出现case穿透现象,即不再比较下面的值,直接执行其他分支的语句,直到break
switch(变量){
case 值:
JAVA语句;
break;
…
}
break语句
a:for(k=0;k<2;k++){
b:for(i=0;i<10;i++){
if(i==5){
break a; //跳出指定循环
}
System.out.println(i);
}
}
//输出结果01234
实参
public static int sum(int a,int b){
return a+b;
}
byte x=1;
byte y=2;
sum(x,y); //形参是int类型可以传byte,相当于int a=x;
break和return
。break终止switch和离他最近的循环
。 return终止离他最近的方法
缺少返回语句
public static int m(){
boolean flag=true;
if(flag){
return 1;
}
}
编译报错,没有返回语句,因为编译器只知道flag是boolean类型,不知道值是true,编译器会认为flag有可能是false,则return语句不会执行
方法重载
和返回值类型、修饰符无关,和方法名自己形参列表有关。
返回值、修饰符类型不同,方法名相同,参数列表相同,视为方法重复,编译不通过。
JVM内存结构
。方法区:类加载器加载class文件到方法区,方法区存的是字节码的存代码片段
。堆内存:new出来的对象和对象的实例变量
。栈:方法调用时为方法在栈内存开辟一块空间,存方法里的局部变量
问题:引用一定是局部变量嘛?
不一定。
栈里的a和u是引用,存储了一个指向对象的内存地址,在main方法里,是局部变量。
堆里的addr也存储了一个指向对象的内存地址,是引用,但它在类体中,方法之外,它属于成员变量或者说实例变量。
类
成员变量不能通过类名访问,要通过对象。
空指针
"空引用”访问实例变量/成员变量时,发生空指针异常
public class Nullpointer{
public static void main(String[] args){
Customer c=new Customer();
System.out.println(c.id);
c=null; //空引用
System.out.println(c.id); //空指针异常
}
}
class Customer{
int id;
}
构造方法
问题:实例变量没有手动赋值的时候,系统会赋默认值,是类加载的时候赋值的吗?
不是,类加载在方法区。而实例变量不能通过类名访问,只能创建对象访问。创建对象在堆内存,先在堆内存new一个对象,才会开辟一块内存空间放实例变量。new对象实际上是在调用构造方法,所以实例变量是在构造方法执行的时候初始化、完成赋值的。
static
在方法体内的变量称为局部变量,在方法体外的变量叫成员变量。
成员变量分为实例变量和静态变量。
静态变量和静态方法都是类相关,通过“类名.”访问,不需要new一个对象。
实例变量和实例方法都是对象相关,通过“引用.”访问,需要先new一个对象。
静态变量在类加载时初始化,不用创建对象。静态变量放在方法区。
实例的一定要用引用.访问
静态的建议用类名.访问,也可以用引用.访问
** 空引用.静态变量不会发生空指针异常。
static静态代码块
类加载时执行,且只执行一次。
一个类中可以编辑多个静态代码块。
public class StaticTest {
static int a,b,c;
static {
a=10;
System.out.println("静态代码块1执行了");
}
public static void main(String[] args) {
System.out.println(a+b+c);
}
static {
b=100;
c=200;
System.out.println("静态代码块2执行了");
}
}
运行结果:
静态代码块1执行了
静态代码块2执行了
310
实例语句块
实例语句块在构造方法之前执行。
public class InstanceTest {
int count;
public InstanceTest() {
System.out.println("构造方法执行");
}
{
count=10;
System.out.println("实例语句块执行了");
}
public static void main(String[] args) {
InstanceTest instanceTest = new InstanceTest();
System.out.println(instanceTest.count);
}
}
运行结果:
实例语句块执行了
构造方法执行
10
this
一个对象,一个this。
this是一个变量,一个引用,存放指向自身的内存地址。
this的代码复用
this(实际参数列表)
用于在同一个类中调用另一个构造方法,且必须是构造器中的第一个语句。
class Date{
private int year;
private int month;
private int day;
public Date(){
this(1970,1,1);
}
public Date(int year,int month,int day){
this.year=year;
this.mouth=month;
this.day=day;
}
}
toString()
System.out.println()里面是引用的时候,默认调用该引用的toString()方法。
public class Test{
public static void main(String[] args){
A a=new A();
System.out.println(a);
//结果等同于System.out.println(a.toString());
//输出结果:A(类名)@十六进制(经过哈希算法处理的引用的内存地址)
}
}
class A{}
方法覆盖override
1.子类对父类方法进行覆盖时,访问权限只能更高不能更低。比如父类方法使用public,子类就不能使用其他的。
2.重写之后的方法不能比之前的方法抛出更多的异常,可以更少。
3.重写之后的两个方法,拥有相同的返回值类型,相同的方法名,相同的参数列表
** 返回值类型是基本数据类型必须相同。
**返回值类型是引用类型,如Animal是Cat的父类,有一个类x中有一个返回值类型为Animal中的方法,x的子类y重写该方法,返回值类型可以为Cat,但不能为Object
4.私有方法无法覆盖。
5.方法覆盖只针对实例方法,静态方法没有意义
原因:
class Animal{
public static void move(){
System.out.println("动物在移动");
}
}
class Cat extends Animal{
public static void move(){
System.out.println("猫在跑");
}
}
Animal a=new Cat(); //多态
a.move();
//静态方法用类名.调用,就算使用引用.,实际还是类名.调用的。
//运行结果是“动物在移动”,因为a是Animal类,调用Animal中的move方法。
Cat c=new Cat();
c.move();
//运行结果是“猫在跑”,但这样的方法重写没有意义,跟在Cat类中另外定义一个新方法效果一样。
多态
多态是指编译的时候一种形态,运行时另一种形态。
向上转型:子--父
向下转型:父--子
class Animal{
public void move(){
System.out println("动物在移动");
}
}
class Cat extends Animal{
public void move(){
System.out println("猫在跑");
}
}
Animal a=new Cat(); //多态
编译时,编译器识别到引用a的类型是Animal,静态绑定Animal里的move()方法。
运行时,创建的对象实际是Cat的对象,所以动态绑定Cat中的move()方法。
instanceof
instanceof可以在运行阶段动态判断引用指向对象的类型。
语法格式
(引用 instanceof 类型)
执行结果为boolean
向下转型时使用instanceof运算符进行判断,可以避免ClassCastEception类型转换异常
super
super()表示在子类的构造方法中调用父类的构造方法,模拟现实世界中先有父亲才有儿子。
class A{
public void A(){
System.out.println("A的无参构造");
}
}
class B extends A{
public void B(){
//super(); 不写也有
System.out.println("B的无参构造");
}
}
new B();
//运行结果:
//A的无参构造
//B的无参构造
原因:B的构造方法中第一行默认有一个super();
如果A类只写了一个有参构造,就不会自动给无参构造方法。如果A有无参构造的时候,B调用无参构造的时候会自动写一行super(),可以不写,默认会有。但A只有有参构造,B的有参构造或者无参构造都必须手动写super(参数)。
class A{
public void A(int i){
System.out.println("A的构造");
}
}
class B extends A{
public void B(){
super(1);
System.out.println("B的无参构造");
}
}
super和this不能共存,有this没super,有super没this。如果子类的一个构造方法中调用了this,那么其他的构造方法总会调用super,也就是说子类调用构造方法,就一定会先调用父类的构造方法。
super调用父类的构造方法,不是创建父类的对象,只是继承了父类的特征。
"super."什么时候不能省略?
子类有和父类同名的属性,并且在子类中想访问父类的那个属性时,super.不能省略。
super不是引用,不保存内存地址,也不指向对象。super只代表当前对象内部的那一块父类特征。
super.方法(实参)代表调用父类方法。
final
final修饰的类无法被继承,final修饰的方法不能被覆盖,final修饰变量只能赋值一次。final修饰的实例变量系统不会赋默认值,必须手动赋值,可以在变量后面等号赋值,也可以在构造方法里赋值。(final修饰的实例变量没有手动赋值会编译报错未初始化,一般实例变量没有赋值,系统会赋初始值,初始化是在构造方法调用时,即new一个对象的时候,构造方法里什么都不写也会默认有一个赋初始值,但final修饰的实例变量系统不管。)
final和static联合修饰的变量叫常量,通常全部字母大写,单词之间下划线连接。
abstract
抽象类无法实例化,不能创建对象。
abstract和final不能联合使用,因为final修饰的类不能被继承,abstract修饰的类就是来被继承的。
抽象类有构造方法,是供子类创建对象用的。因为子类调用构造方法时会先调用父类构造方法(super())。
抽象方法以分号结尾,没有方法体,没有具体的实现。
抽象类中可以没有抽象方法,但抽象方法必须在抽象类中。父类是抽象类,父类中有一个抽象方法,子类要继承父类的抽象方法时,两种方式:1.子类也是抽象类,重写父类的抽象方法,抽象类中可以有抽象方法。2.子类是非抽象类,重写父类的抽象方法时变成非抽象方法。
判断:没有方法体的方法都是抽象方法。
错误。Object类中就有很多没有方法体的方法,但是不是抽象方法,没有abstract关键字修饰,但有native修饰,代表调用c++代码。
接口
接口只能用public修饰,但public不是必须写,可以省略。
接口也是一种引用数据类型,编译生成.class
接口可以继承,并且支持多继承。一个接口可以继承多个接口。
接口里只有常量和抽象方法。
接口里所有的元素都是public修饰。
接口里的抽象方法,public abstract可以省略。
接口里的常量,public static final可以省略,随意写一个变量int i=1;都是常量,会自动在前面加上public static final。
接口不能实例化,但是可以用多态。
如:
public Interface A{}
public class B implements A{}
public class Test{
public static void main(String[] args){
//可以看做继承
A a=new B();
}
}
在编译阶段,接口和接口做强制类型转换时,以及类强转成接口时,不是继承关系也不会报错。但是运行阶段可能出现ClassCastEception。运行阶段有继承关系才能强转。
lang包
lang包下的直接子类不用导入,会自动导入,如String类。
访问控制修饰符
protected 本类 同包 子类
default 本类 同包
接口和类只能用public和default修饰。
Object里的常用方法
equals方法
Object类里提供的equals比较的是对象的内存地址,想比较对象的内容要自己重写。
基本数据类型判断相同用==,引用数据类型用.equals。String类也是引用数据类型,比较字符串相同要用.equals,String类已经重写了equals方法。因为类都有构造方法,如果用new String("abc")创建的对象用==比较的是内存地址,如果是String s="ab"可以用==,但是不知道创建的方式会是什么,所以都用.equals
前两行为模板
finalize方法
protected修饰
不需要程序员手动调用,JVM的垃圾回收器在回收前自动调用,程序员只需要重写。相当于给程序员准备的一个垃圾回收时机。
注意:垃圾回收器不轻易启动,时间没到,垃圾太少,种种原因,可能不会启动,该方法可能不会被调用。
System.gc()可以建议垃圾回收器启动,但只是建议,不一定启动。
hashCode方法
底层调用c++,返回int类型对象的哈希码,实际上是对象的内存地址经过哈希算法得出来的值。
内部类
在一个类里再定义在一个类
静态内部类,相当于静态变量,用static修饰
实例内部类,相当于实例变量
先实例外面的类才能实例化
new Test().new Inner()
局部内部类,相当于局部变量,定义在方法里的类
匿名内部类
是局部内部类的一种
因为接口不能new对象,所以要创建接口的实现类,再实例化实现类的对象。匿名内部类就是不用再创建实现类,直接new 接口名(){具体实现}。
缺点是只能使用一次。
数组
如果数组存储的是java对象,实际上存的是引用,是内存地址。
数组一旦创建,长度不可变。
数组自带length属性,表示元素个数。
数组中元素的内存地址连续。
数组中第一个元素的内存地址,作为整个数组对象的内存地址。
优点:
查询效率高,因为知道第一个元素的内存地址,每个元素类型相同所以占用空间大小相同,知道下标就是知道偏移量,可以通过数学表达式计算出任意位置的元素的内存地址。
缺点:
1.为了保持内存地址的连续性,随机增加或删除一个元素时,涉及到一系列元素向前或向后移动的操作。
2.不能存储大数据量,因为很难在内存中找到一块特别大的连续内存。
一维数组初始化
静态初始化
int[] arr1={23,88,65}
动态初始化
int[] arr2=new int[5]
方法参数是数组
动态
printArray(new int[5]);
静态
int[] arr={1,2,3};
printArray(arr);
-----------------
printArray(new int[]{1,2,3});
main方法中的参数
JVM调用main方法自动传过来的数组默认长度为0,也就是数组创建了但里面没存东西。
String[] s1={};
String[] s2=new String[0];
该数组是留给用户的,用户可以从控制台传递参数
class Test03{
public static void main(String[] args){}
}
例如这样运行
java Test03 AAA bbb ccc
以空格划分,把AAA bbb ccc作为元素添加到args了。
在idea里面run选择edit configuration,在program configuration里面填AAA bbb ccc效果相同。
数组的扩容
创建一个更大的数组,把原来数组拷贝到新数组中。
System.arraycopy(源数组,从几号索引开始拷贝,目标数组,拷贝到新数组的几号索引,拷贝的长度)
二分法查找
Arrays.binarySearch(int 数组,查找的元素)
返回一个int值,为查找元素的下标。没有找到返回一个负数。
作用:查找某个元素的下标。
适用于已经排好序的数组。
(首元素下标+末元素下标)/2=中间元素下标,用中间元素和目标元素比较是否相等,如果不相等。中间元素大于目标元素,(原首元素下标+原中间元素下标-1)/2,再次得到中间元素,与目标元素比较,直到找到目标元素;第一次比较如果中间元素<目标元素,(中间元素下标+1)+末尾元素/2,同上。
String类
因为字符串使用频繁,在字符串常量池中创建可以提高效率。如s1="abc";在常量池中创建了"abc",s2="abc"就不用再次在常量池中创建新对象,s1和s2存储的内存地址相同。
存储原理
凡是双引号“”都会在方法区的字符串常量池里创建对象。
String s1=“abcdef”
在字符串常量池中创建“abcdef”,s1中保存字符串常量池中对象的内存地址。
String s2="abcdef"+"xy"
已有的"abcdef"可以直接拿来用,在常量池创建"xy",因为拼接操作,创建新的字符串"abcdefxy",s2中保存新字符串内存地址
String s3=new String("xy")
堆中创建String对象,里面存放"xy"的内存地址,s3中存放String对象内存地址。
构造方法
常用方法
char charAt(int index) 输入索引返回对象字符
int compareTo(String s) 字典顺序比较字符串
相等返回0 前小后大返回-1 前大后小返回1
boolean contains(CharSequence s)是否包含子字符串
StringBuffer和StringBuilder
String的底层是final byte[]
StringBuffer底层是byte[]
所以String是不可变的,在需要频繁字符串拼接的时候如果使用String用+拼接,会占用方法区里的大量内存。
使用StringBuffer的append()方法,底层调用了System.arrayCopy()进行数组拷贝,从而实现数组的扩容。
StringBuffer的空参构造方法创建的是byte[16],也可以使用StringBuffer(int capacity)指定容量。如果不指定容量会自动扩容,但是最好一开始定义合适的容量,避免频繁扩容。
StringBuffer和StringBuilder区别
StringBuffer里的方法都是synchronized修饰的,StringBuilder里的方法没有synchronized修饰。
代表StringBuffer是多线程安全的,StringBuilder是多线程不安全的。
String类中唯一静态方法valueOf()
System.out.println()底层调用了String.value Of(),这个方法底层调用了toString(),所以控制台输出的都是字符串。
包装类
基本数据类型不够用了,所以需要包装类。
jdk1.5之前,例如一个方法需要一个Object类型参数,不能传入如int的基本数据类型参数。
装箱:基本数据类型--->引用数据类型
使用new对象的方式把基本数据类型作为构造方法的参数传入可以完成装箱。如Interger i
=new Integer(123);
拆箱:引用数据类型--->基本数据类型
以下为Number类中的方法,Number是一个抽象类,它是所有包装类数字类的父类。
int a=i.intValue(); //拆箱
Integer
有两种构造方法,其他包装类相同。
Integer a=new Integer(123);
Integer b=new Integer("123");
访问包装类中的常量获取最大值、最小值
Integer.MAX_VALUE
Integer.MIN_VALUE
jdk1.5之后支持自动装箱和拆箱
Integer a=100; //int类型自动转成Integer
int b=a; //Integer自动转成int
Integer a=100;
Integer b=100;
System.out.println(a==b); //true
Integer a=128;
Integer b=128;
System.out.println(a==b); //false
原因:
int String Integer相互转换
可能出现的异常
int i=Integer.parseInt("123");
Integer.parseInt("中文");
//出现NumberFormatException
日期类
java.util.date
new Date()
new Date(long 毫秒数)
输出Date对象的引用,会自动调用Date重新的toString()输出时间
System.currentTimeMillis()
获取1970年1月1日0时0分0秒至今的毫秒数。
java.text.SimpleDateFormat
SimpleDateFormat sdf=
new SimpleDateFormat("日期格式模板");
日期--->字符串
String s=sdf.format(new Date());
字符串--->日期
Date d=sdf.parse("和日期格式模板对应的日期字符串");
数字类
DecimalFormat
DecimalFormat df=
new DecimalFormat("数字格式化")
# 任意数字
, 千字符
. 小数点
###,###.## ###,###.0000 保留四位小数
1,234.56
BigDecimal
BigDecimal bd=new BigDecimal(100);
BigDecimal bd1=new BigDecimal(100);
这里不是普通的100,是精度很高的100
bd是一个引用不能直接用运算符如+计算
bd.add(bd2) 加法
bd2.divide(bd) 除法
枚举
枚举是引用数据类型,编译会生成class
枚举值相当于常量
enum 枚举名{
枚举值,枚举值
}
public class Test01 {
public static void main(String[] args) {
Seasons season=Seasons.summer;
switch (season){//switch(Seasons.summer)
case spring:
System.out.println("春天");
break;
case summer:
System.out.println("夏天");
break;
case autumn:
System.out.println("秋天");
break;
case winter:
System.out.println("冬天");
break;
}
}
}
enum Seasons{
spring,summer,autumn,winter
}
异常
异常在JAVA中以类的形式存在,可以创建异常对象。
我们看到的控制台异常信息,就是在程序发生异常时,JVM创建异常对象后抛出,输出到控制台。
异常的父类是Throwable,Throwable父类是Object。
Throwable下面有两个子类,Error和Exception。不管是错误还是异常都是可抛出的。
Error:错误只要发生就会终止程序,退出JVM。错误是不能被处理的。
Exception:有两个分支,一个是直接子类,一个是RuntimeException。直接子类都是编译时异常,RuntimeException是运行时异常。
问题:编译时异常是编译阶段发生的吗
不是,是在编写程序时必须预先处理的,不处理编译报错。
try…catch
catch语句可以有多条
catch的异常从上到下要从小到大
try{
}
catch(IOException){}
catch(FileNotFoundException){}
报错
java8新特性
catch(FileNotFoundException | Nullpointer Exception e){}
异常中常用方法
1.exception.getMessage()
String msg=exception.getMessage();
获取简单异常描述信息
Nullpointer exception e=new NullPointerException("空指针异常");
System.out.println(e.getMessage());
输出的是构造方法里的字符串
2.exception.printStackTrace()
打印异常追踪的堆栈信息
java采用异步线程打印
finally语句
和try一起使用,finally语句中的代码是最后执行的,而且一定会执行,即使try中语句有异常。
作用:完成源的释放/关闭。
因为如果关闭资源的语句放在try中,如果前面有异常,就不会执行后面的语句,也就不会关闭资源,在finally里完成源的释放有保障。
退出JVM,finally不执行
面试题
自定义异常
SUN定义的异常在之后的业务中可能不够用,可能有一些和业务挂钩的异常需要自定义。
继承Exception或者RuntimeException。
写一个无参构造方法,一个有参构造方法。
注意:子类对父类方法进行覆盖时,只能抛出比父类更少、更小的异常,父类方法没抛异常,子类方法覆盖不能抛异常,但可以抛RuntimeException。
getMessage()为什么可以拿到构造方法里传的字符串?
先自定义一个异常类
自定义异常时写一个无参构造方法,一个有参构造方法,有参构造的第一行super(s),不写这一行会自动调用父类的无参构造方法。
点进去
s传给了Exception的有参构造
再点进去
继续传给了Throwable的有参构造,并赋值给detailMessage
detailMessage是一个实例变量
在Throwable里的getMessage()方法返回了detailMessage的值,也就是有参构造里传入的s
集合
数组既可以存基本数据类型,也可以存对象的内存地址。集合只能存内存地址。
不同的集合底层是不同的数据结构,数据结构是存储数据的方式,如数组、二叉树、链表都是数据结构。
Collection集合
Collection接口的父类是Iterable接口,Iterable接口里有一个iterator()方法,这个方法的返回值类型是一个叫Iterator的接口。
(接口)Iterator it=
Collection将来的子类构造的对象.iterator()
it是一个迭代器
Iterator接口里有三个方法,it调用这些方法可以对Collection将来的子类构造的对象进行迭代。
如果使用Collection的方法更改集合,就要创建新的迭代器,否则调用next()方法时会出异常。使用迭代器自己的remove()方法没关系,会删除当前指向的元素。
Collection接口有两个常用的子接口,List接口和Set接口。
- List接口:有序、可重复。有序代表存进集合是什么顺序,取出来就是什么顺序。有序还体现在List集合有下标。
- Set接口:无序,不可重复。无序代表存进集合是什么顺序,取出来不一定是这个顺序。Set集合没有下标。
Collection的contains()和remove()方法底层调用了equals()方法,所以会比对和操作与传入的参数“值”相同的元素。放入集合的对象都应该重写equals()方法。
运行结果
0
remove()可以传对象或者索引,这是两个不同的remove
如果向集合中添加了两个值同为“hello”的对象,用remove("hello")删除时,只删除其中一个。
List集合
特有的方法add(索引,元素)
List list=new Arraylist();
list.add("a");
list.add(1,"king");
list.add("b");
//输出的代码省略
运行结果
a
king
b
这种add()方法使用的不多,因为效率低。Arraylist底层是数组,因为内存是连续的,会涉及一系列元素的向前或向后移动。Collection的add()方法添加在末尾,不影响。
ArrayList
- 底层是object数组,默认初始化长度10,每次扩容到原来的1.5倍
- 不是线程安全的
- 因为数组的特性,查询效率高,随机增删效率低。但一般都在集合末尾添加元素,所以ArrayList是使用最多的。
- ArrayList有两种构造方法,一种是空参构造,另一种是传一个Collection数组
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();
if ((size = elementData.length) != 0) {
// c.toArray might (incorrectly) not return Object[] (see 6260652)
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// replace with empty array.
this.elementData = EMPTY_ELEMENTDATA;
}
}
LinkedList
以下是单向链表,LinkedList实现是双向链表
双向链表
LinkedList源码
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
transient int size = 0;
//指向链表中第一个节点
transient Node<E> first;
//指向链表中第二个节点
transient Node<E> last;
}
//LinkedList部分代码
//内部类
private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
//LinkedList部分代码
//add方法
public boolean add(E e) {
linkLast(e);
return true;
}
//LinkedList部分代码
void linkLast(E e) {
final Node<E> l = last;
final Node<E> newNode = new Node<>(l, e, null);
last = newNode;
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
modCount++;
}
Vector
初始化容量默认10
每次扩容至原来的2倍
vector源码
public Vector() {
//空参构造调有参构造
this(10);
}
public Vector(int initialCapacity) {
//有参构造调另一个有参构造
this(initialCapacity, 0);
}
public Vector(int initialCapacity, int capacityIncrement) {
super();
//初始化容量非法
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
//创建容量为10的数组
this.elementData = new Object[initialCapacity];
//容量扩容0
this.capacityIncrement = capacityIncrement;
}
public synchronized boolean add(E e) {
modCount++;
//elementCount是实例变量,创建vector对象后自动赋值0
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = e;
return true;
}
private void ensureCapacityHelper(int minCapacity) {
// overflow-conscious code
//集合的size()是元素个数,数组的length是数组长度
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
capacityIncrement : oldCapacity);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
elementData = Arrays.copyOf(elementData, newCapacity);
}
public static <T> T[] copyOf(T[] original, int newLength) {
return (T[]) copyOf(original, newLength, original.getClass());
}
public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
@SuppressWarnings("unchecked")
T[] copy = ((Object)newType == (Object)Object[].class)
? (T[]) new Object[newLength]
: (T[]) Array.newInstance(newType.getComponentType(), newLength);
System.arraycopy(original, 0, copy, 0,
Math.min(original.length, newLength));
return copy;
}
//调用底层c++
public static native void arraycopy(Object src, int srcPos,
Object dest, int destPos,
int length);
问题:如何把线程不安全的ArrayList变成线程安全的
java.util.collections工具类(不是collection接口)
List list=new ArrayList();
Collections.synchronizedList(list);
泛型
泛型是jdk5之后的新特性
jdk8之后引用了自动类型推断(又称钻石表达式)
List<String> list1=new ArrayList<String>();
List<String> list2=new ArrayList<>();//自动类型推断
自定义泛型
<>里是标识符,可以随便写
源码中一般是E(Element)或者T(Type)
类定义了泛型,但创建对象时不使用,默认Object类型
Map集合
常见方法
1.boolean containsKey(Object key)
2.boolean containsValue(Object value)
这两个方法底层调用的都是equals(),所以key和value使用的引用类型要重写equals()
3.Set<Object> keySet() 返回一个装有全部key的set集合
Map<Integer,String> map=new HashMap();
map.put(1,"zhangsan");
map.put(2,"lisi");
map.put(3,"wangwu");
map.put(4,"zhaoliu");
//先拿到key,根据key获取value
Set<Integer> keys = map.keySet();
//迭代器
Iterator<Integer> it = keys.iterator();
while (it.hasNext()){
Integer next = it.next();
System.out.println(map.get(next));
}
//增强for
for(Integer key:keys){
System.out.println(map.get(key));
}
4.Set<Map.Entry<K, V>> entrySet() 返回一个set集合,元素类型是Map.Entry<K, V>
Map.Entry是一个接口
HashMap中有一个内部类Node实现了这个接口
所以这个方法其实返回的是元素为Node类型的set集合
//该类重写的toString()
public final String toString() { return key + "=" + value; }
Set<Map.Entry<Integer, String>> set = map.entrySet();
//迭代器
Iterator<Map.Entry<Integer, String>> iterator = set.iterator();
while (iterator.hasNext()){
Map.Entry<Integer, String> Node = iterator.next();
System.out.println(Node);
}
//增强for
for(Map.Entry<Integer,String> node: set){
//也可以使用node.getKey(),getValue()分别获取
//这两个方法是Node从Map.Entry继承的
System.out.println(node);
}
HashMap
1.HashMap底层是哈希表/散列表的数据结构
2.哈希表是数组和单向链表的结合
3.HashMap集合的默认初始化容量是16,默认加载因子是0.75
默认加载因子是当HashMap集合容量达到75%就开始扩容。
注意:HashMap初始化容量必须是2的次幂
4.jdk8后的新特性:如果哈希表单向链表中元素超过8,单向链表会转成红黑树数据结构;当红黑树上的节点小于6,会重新把红黑树变成单向链表数据结构。
5.HashMap扩容之后是原来的2倍。
6.HashMap的key和value都可以为空,但是只能有一个为null的key,重复添加value会覆盖。
7.通过get(null)可以找到对应的value
8.线程不安全
9.相同的key,hash值一定相同,因为hash值是hashcode()方法的执行结果,hash值可以通过hash算法转换成数组下标 tab[i = (n - 1) & hash] (n是数组长度)
HashSet源码
private static final Object PRESENT = new Object();
public HashSet() {
map = new HashMap<>();
}
//HashSet的add方法调用的是HashMap的put方法
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
HashMap源码
第一次put元素执行步骤
public class HashMap<K,V>{
//HashMap底层是一个数组
transient Node<K,V>[] table;
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;//哈希值,key的hashCode()方法的执行结果,通过哈希算法可以转换成下标
final K key;
V value;
Node<K,V> next;
}
}
//HashMap的put方法
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
static final int hash(Object key) {
int h;
// ^ 异或运算 两个不同才为1
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
//HashMap中一个实例变量,new对象后默认为null
transient Node<K,V>[] table;
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
//第一个元素put,判断Node<K,V>[] table,是否为空,第一次空的,调用resize()扩容
//扩容之后数组长度赋值给n,n=16
n = (tab = resize()).length;
}
//实例变量,容量的阈值,超过这个值就要扩容,初始化为0
int threshold;
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
tatic final float DEFAULT_LOAD_FACTOR = 0.75f;
final Node<K,V>[] resize() {
//把原来的数组复制一份
Node<K,V>[] oldTab = table;
//原来的数组为空,就给原容量赋值0
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//阈值赋给原来的阈值
int oldThr = threshold;
//定义新容量(没赋值),新阈值赋值0
int newCap, newThr = 0;
//判断原容量是否>0,第一次put,oldCap=0,不走这个if
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
//判断原阈值是否>0,第一次put这条分支也不走
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
//第一次put直接进else
else { // zero initial threshold signifies using defaults
//新容量赋值16,新阈值12
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
//新阈值不为0,这条不走
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
//实例变量的阈值赋值成12
threshold = newThr;
//创建容量16的新数组
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
//第一次put不走
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
//直接返回新数组
return newTab;
}
//接着走没走完的putVal()方法
//p是一个Node,tab是一个Node数组,n=16,15&hash会得到一个0-15的int值
//算出来的值赋值给i,并访问tab数组下标为i的元素,赋值给p
//第一次数组所以下标位置都为null
if ((p = tab[i = (n - 1) & hash]) == null)
//在下标为i的位置创建节点
tab[i] = newNode(hash, key, value, null);
else{...} //此处不走else 代码省略
类比
int i=0;
System.out.println(i=9);
运行结果 9
int[] arr1=new int[10];
//以下为局部变量,不会自动初始化,所以手动初始化0
int p=0;
int i=0;
int n=0;
arr1[3]=1;
//if ((p = tab[i = (n - 1) & hash]) == null)
// -1 & 3 =3 负数用补码&运算
//1 0 0 0 0 0 0 1 -1原码
//1 1 1 1 1 1 1 1 -1补码
//0 0 0 0 0 0 1 1 3原码
//0 0 0 0 0 0 1 1 -1 & 3
int i1 = arr1[i = (n - 1) & 3];
System.out.println(i1);
运行结果 1
Student[] students1=new Student[10];
Student student=new Student();
System.out.println((student=students1[0])==null);
运行结果 true
++modCount;
//添加第12个元素之后就要扩容了
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
不是第一次put,但算出的数组下标还没有元素时
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
//虽然没有进入这个分支,但是tab和n的赋值在这里完成
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//如果该数组下标为空,可以直接添加新节点
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
//........省略一段没执行的代码
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
put(k,v)和get(k)实现原理
注意:放在HashSet和HashMap的k部分的元素类型都要重写hashCode()和equals()。用IDEA一起重写。
Student类
//没有重写hashCode()和equals()
public class Student {
private String name;
public Student() {
}
public Student(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
HashSet<Student> set=new HashSet();
Student s1=new Student("zhangsan");
Student s2=new Student("zhangsan");
set.add(s1);
set.add(s2);
System.out.println(s1.hashCode()); //356573597
System.out.println(s2.hashCode()); //1735600054
System.out.println(set.size()); //2
HashSet的add()方法底层调用的是HashMap的put()方法,put()方法原理调用hashCode()得出来不同的hashCode,就会得出不同的下标。(不同的HashCode也有概率得到相同下标,称为哈希碰撞)该下标没有元素时,不会调用equals()方法,直接存进去。
这里因为没有重写两个方法,把相同的两个值存进HashSet了,这是不合理的。
重写之后的hashCode相同。
HashTable
1.HashTable的key和value都不能为空。
2.初始化容量11,扩容因子0.75
3.扩容为原来的2倍+1
Propreties
1.key和value只能是String
2.线程安全。
3.setPropreties()底层调用HashTable的put()方法,getPropreties()底层调用get()
TreeMap
1.无序,不可重复,可自动排序
2.放在TreeMap中的类必须实现java.lang.Comparable接口,重写CompareTo方法才能排序,不然会报错ClassCastException;或者创造比较器实现java.util.Comparator接口
3.如果比较规则只有一种,且不会轻易改变,建议实现Comparable接口。String和Integer都是实现的Comparable接口,因为数字和字符串的比较规则只有一种。如果比较规则有多个,并且需要来回切换,建议实现Comparator接口。这符合OCP原则(open closed principle开放封闭原则,对扩展开放,对修改关闭。体现在可以创建新的不同规则的比较器,而不是去修改原来的比较器)
4.TreeMap/TreeSet是一种平衡二叉树,遵循左小右大原则存放
5.遍历二叉树有三种方式:
前序遍历 根左右
中序遍历 左根右
后序遍历 左右根
注意:前中后指的是根的位置,根在前面是前序,根在中间是中序...
6.TreeMap/TreeSet是中序遍历
//TreeMap源码
//空参构造 比较器默认空
public TreeMap() {
comparator = null;
}
//也可以调用有参构造传一个比较器
public TreeMap(Comparator<? super K> comparator) {
this.comparator = comparator;
}
private final Comparator<? super K> comparator;//比较器
//TReeMap的put方法源码
public V put(K key, V value) {
//声明唯一总根节点
Entry<K,V> t = root;
if (t == null) {
compare(key, key); // type (and possibly null) check
//第一次传进来的作为根,第三个参数是parent根没有parent,所以为null
root = new Entry<>(key, value, null);
size = 1;
modCount++;
return null;
}
int cmp;
//声明根
Entry<K,V> parent;
Comparator<? super K> cpr = comparator;
//如果采用空参构造创建的比较器为空,就走else分支
//如果构造方法里传了比较器对象,走这个分支
if (cpr != null) {
do {
parent = t;
cmp = cpr.compare(key, t.key);
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else
return t.setValue(value);
} while (t != null);
}
else {
//这里判断key是否为空是因为,下面要用key调用compareTo方法
//上面一种是比较器调用compare方法,所以不用判断key是否空
if (key == null)
throw new NullPointerException();
@SuppressWarnings("unchecked")
//TreeSet中add的就是key
//这里有强制类型转换
//如果传进去的对象的类没有实现Comparable接口,就不能类型转换
Comparable<? super K> k = (Comparable<? super K>) key;
//采用do...while而不是while(){}原因:
//第一次的t是根节点,如果走到这里根节点一定非空,不用判断,所以第一次直接do
do {
//t是父节点
parent = t;
//k是传进来的参数key,和根节点的key比较
cmp = k.compareTo(t.key);
if (cmp < 0)
//把当前父节点的左节点赋值给父节点,左节点变成新的根
t = t.left;
else if (cmp > 0)
t = t.right;
else
//key相同就用新的value覆盖
return t.setValue(value);
} while (t != null);
}
Entry<K,V> e = new Entry<>(key, value, parent);
if (cmp < 0)
parent.left = e;
else
parent.right = e;
fixAfterInsertion(e);
size++;
modCount++;
return null;
}
实现比较器
1.创建一个类实现java.util.Comparator接口,重写compare()方法
public class Cat {
int age;
public Cat(int age){
this.age=age;
}
@Override
public String toString() {
return "Cat{" +
"age=" + age +
'}';
}
}
class CatComparator implements Comparator<Cat>{
@Override
public int compare(Cat o1, Cat o2) {
return o1.age-o2.age;
}
}
public static void main(String[] args) {
TreeSet<Cat> treeSet=new TreeSet<>(new CatComparator());
treeSet.add(new Cat(7));
treeSet.add(new Cat(1));
treeSet.add(new Cat(4));
treeSet.add(new Cat(5));
for(Cat c:treeSet){
System.out.println(c);
}
}
运行结果
Cat{age=1}
Cat{age=4}
Cat{age=5}
Cat{age=7}
2.不写实现类,采用匿名内部类
public static void main(String[] args) {
TreeSet<Cat> treeSet=new TreeSet<>(new Comparator<Cat>(){
@Override
public int compare(Cat o1, Cat o2) {
return o1.age-o2.age;
}
});
treeSet.add(new Cat(7));
treeSet.add(new Cat(1));
treeSet.add(new Cat(4));
treeSet.add(new Cat(5));
for(Cat c:treeSet){
System.out.println(c);
}
}
Integer和String的比较器已经写好升序排列,如果想降序排列可以重写比较器
public static void main(String[] args) {
TreeSet<Integer> tree=new TreeSet<>(new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
//TreeSet的add方法调用TreeMAp的put方法
//put方法中传入compare()方法参数的顺序是compare(新元素,父节点)
//按照Integer默认的compare方法,如果compare()返回的int大于0,
//证明新元素比当前父节点大,要放在右子树
//这里return o2-o1如果>0,t=t.right
//代表父节点如果比新元素大,把新元素放在右子树,也就是降序排列,小的在后面
return o2-o1;
}
});
tree.add(1);
tree.add(100);
tree.add(30);
tree.add(0);
for(Integer i:tree){
System.out.println(i);
}
}
Integer的compare()方法
public static int compare(int x, int y) {
return (x < y) ? -1 : ((x == y) ? 0 : 1);
}
String的compare()方法
public int compare(String s1, String s2) {
int n1 = s1.length();
int n2 = s2.length();
int min = Math.min(n1, n2);
for (int i = 0; i < min; i++) {
char c1 = s1.charAt(i);
char c2 = s2.charAt(i);
if (c1 != c2) {
c1 = Character.toUpperCase(c1);
c2 = Character.toUpperCase(c2);
if (c1 != c2) {
c1 = Character.toLowerCase(c1);
c2 = Character.toLowerCase(c2);
if (c1 != c2) {
// No overflow because of numeric promotion
return c1 - c2;
}
}
}
}
return n1 - n2;
}
Collections工具类
Collections.sort()方法,参数传一个List集合,可以对List排序。前提是放到List集合中的对象所属类实现了Comparable接口。
public static void main(String[] args) {
List<Student> list=new ArrayList<>();
list.add(new Student("zhangsan"));
list.add(new Student("lisi"));
Collections.sort(list);
for(Student s:list){
System.out.println(s);
}
}
运行结果(按名字排序了,没排序前结果按添加顺序输出,即zhangsan lisi)
Student{name='lisi'}
Student{name='zhangsan'}
问题:因为sort()方法参数只能是List集合,如果想要给Set集合排序怎么办?
使用ArrayList的有参构造,把Set集合作为参数传进去,再用sort排序
流
流的分类
1.按照流的方向分,以内存为参照物
从硬盘到内存中去,叫做“输入”,或者叫做“读”;
从内存出去到硬盘。叫做“输出”,或者叫做“写”
2.按照读取方式分类
- 按字节读取,每次读取1个字节。这种流是万能的,可以读取任何类型的文件,如:文本文档、图片、视频、声音文件等
假设file.txt中文件内容 a中国人bc,windows中英文1个字节,中文两个字节
第一次读:一个字节,正好读到'a'
第二次读:一个字节,正好读到“中”的一半
第三次读:一个字节,正好读到“中”的另一办
- 按字符读取,一次读取一个字符。这种流只能读文本文档txt,连word都不能读
第一次读:字符'a'
第二次读:字符“中”
四大家族
InputStream 字节输入流
OutputStream 字节输出流
Reader 字符输入流
Writer 字符输出流
1.Java中的类,以Stream结尾的都是字节流,以Reader或Writer结尾都是字符流
2.以上四个都是抽象类
3.它们都实现了Closeable接口,接口中有close方法,也就是说它们都是可关闭的。
4.输出流都实现了Flushable接口,可刷新的,都有flush()方法,输入流都没有实现这个接口。
输出流在最终输出之后,要记得flush()刷新。
刷新表示将管道/通道中剩余未输出数据强行输出。
刷新的作用就是清空管道。
如果没有flush(),可能丢失数据。
FileInputStream
父类是InputStream
read()
这种read()方法一次只能读取一个字节,需要内存和硬盘频繁的交互,效率太低。
public class FileInputStreamTest01 {
public static void main(String[] args) {
FileInputStream fis=null;
try {
//相对路径
//IDEA的当前路径默认是工程的根
fis=new FileInputStream("src/com/whmc/io/tempfile");
int read=0;
//read()方法返回值是字符的ASII码值,空格也有ASII码,是32
//读不到了返回-1,读不到和空格是两个概念
while ((read=fis.read())!=-1){
System.out.println(read);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (fis != null) {
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
采用byte数组,一次可以读取数组.length个字节,返回值是读取到的字节数量,没有读到返回-1
//tempfile内容:sfhvjjjnd
fis=new FileInputStream("src/com/whmc/io/tempfile");
byte[] bytes=new byte[4];
int read=0;
while ((read=fis.read(bytes))!=-1){
System.out.println(read);
System.out.println(new String(bytes));
}
运行结果
4
sfhv
4
jjjn
1
djjn
但是我们希望字符串只保存读到的元素,所以可以用另一个构造方法
while ((read=fis.read(bytes))!=-1){
System.out.println(read);
System.out.println(new String(bytes,0,read));
}
运行结果
4
sfhv
4
jjjn
1
d
其他常用方法
1.int available() 返回流当中没有读到的剩余字节数量
fis=new FileInputStream("src/com/whmc/io/tempfile");
//在流刚创建的时候就获取剩余字节数,就可以把它作为数组长度,一次性读取文件内容
//这种方法不适合大文件,因为数组内存是连续的
int available=fis.available();
byte[] bytes=new byte[available];
2.int skip(long n) 跳过几个字节不读
//文件内容:sfhvjjjnd
fis=new FileInputStream("src/com/whmc/io/tempfile");
fis.skip(3);
System.out.println(fis.read());
运行结果:118(v的ASII码)
FileOutputStream
父类是OutputStream
1.第一种write(),参数为int
public class FileOutPutStreamTest01 {
public static void main(String[] args) {
FileOutputStream fos=null;
//文件不存在会自动创建
try {
fos=new FileOutputStream("src/com/whmc/io/testfile");
fos.write(97);
fos.write(98);
fos.write(99);
fos.write(100);
//写完之后最后一定要刷新
fos.flush();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (fos != null) {
try {
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
2.第二种write(),参数为byte[]
fos=new FileOutputStream("src/com/whmc/io/testfile");
byte[] bytes={97,98,99,100};
fos.write(bytes);
3.第三种wrire(),参数为byte[],iny off,int len。把byte[]的一部分写出
fos=new FileOutputStream("src/com/whmc/io/testfile");
byte[] bytes={100,101,102};
fos.write(bytes,0,2);
这三种方式都是把原有文件的内容清空再写的
想要在文件末尾添加,采用FileOutputStream的另一个构造方法,参数里的boolean,true表示在末尾追加,false表示清空文件从开头覆盖。
fos=new FileOutputStream("src/com/whmc/io/testfile",true);
文件复制
public class Copy01 {
public static void main(String[] args) {
FileInputStream fis=null;
FileOutputStream fos=null;
try {
fis=new FileInputStream("C:\\Users\\13155\\Downloads\\mate60.jpg");
fos=new FileOutputStream("D:\\mate60.jpg");
//一次读一个字节
byte[] bytes=new byte[1024];
int read=0;
while ((read=fis.read(bytes))!=-1){
//读到多少写多少字节
fos.write(bytes,0,read);
}
fos.flush();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}finally {
//两个流不要放在一个try里关,因为如果有一个有异常,另一个可能关不了
try {
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
FileReader
父类是InputStreamReader,InputStreamReader的父类是Reader
一次读取一个字符
public class FileReaderTest {
public static void main(String[] args) {
FileReader fr=null;
try {
fr=new FileReader("src/com/whmc/io/testfile");
char[] chars=new char[4];
int read=0;
while ((read=fr.read(chars))!=-1){
System.out.println(new String(chars,0,read));
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (fr != null) {
try {
fr.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
运行结果
Chin
a中国
如果用FileInputStraem,长度为4的byte[]读运行结果为:
Chin
a中
国
FileWriter
父类是OutputStreamWriter,OutputStreamWriter的父类是Writer
ublic class FileWriterTest {
public static void main(String[] args) {
FileWriter fw=null;
try {
fw=new FileWriter("src/com/whmc/io/testfile");
char[] chars={'J','A','V','A','软','件','开','发'};
//可以从字符数组读
fw.write(chars);
//也可以从字符串读
fw.write("\n");
fw.write("工程师");
fw.flush();
} catch (IOException e) {
e.printStackTrace();
}finally {
if (fw != null) {
try {
fw.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
普通文本复制
能用记事本打开、编辑的就是普通文本,和文件后缀无关。.java文件也是普通文本
public class Copy02 {
public static void main(String[] args) {
FileReader fr=null;
FileWriter fw=null;
try {
fr=new FileReader("src/com/whmc/io/FileWriterTest.java");
fw=new FileWriter("FileWriterTest.java");
//一次读1kb,因为char是两个字节
char[] chars=new char[512];
int read=0;
while ((read=fr.read(chars))!=-1){
fw.write(chars,0,read);
}
fw.flush();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
fw.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
fr.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
BufferedReader
带有缓冲区的字符输入流
使用这个流的时候不需要自定义char数组或者byte数组,自带缓冲
public class BufferedReaderTest01 {
public static void main(String[] args) throws Exception {
FileReader fr=new FileReader("FileWriterTest.java");
//当一个流的构造方法中需要一个流的时候,被传进来的流叫:节点流
//外部负责包装的这个流叫:包装流,或者处理流
//像当前程序,FileReader是节点流,BufferedReader是包装流
BufferedReader br=new BufferedReader(fr);
String s=null;
//br.readLine()读取一个文本行,但不带换行符
while((s=br.readLine())!=null){
System.out.println(s);
}
//对于包装流来说,只用关闭最外层的流,里面的节点流会自动关闭,可以看源代码
br.close();
}
}
节点流自动关闭源代码
//BufferedReader构造方法,把节点流作为参数传入
public BufferedReader(Reader in) {
this(in, defaultCharBufferSize);
}
//调用另一个构造方法,把节点流赋值给成员变量in
public BufferedReader(Reader in, int sz) {
super(in);
if (sz <= 0)
throw new IllegalArgumentException("Buffer size <= 0");
//in是成员变量
this.in = in;
cb = new char[sz];
nextChar = nChars = 0;
}
public class BufferedReader extends Reader {
private Reader in;
}
//BufferedReader的close方法
public void close() throws IOException {
synchronized (lock) {
if (in == null)
return;
try {
//在这里关闭的节点流
in.close();
} finally {
in = null;
cb = null;
}
}
}
把字节流FileInputStream通过转换流InputStreamReader变成字符流,包装进BufferedReader
public class BufferedREaderTest02 {
public static void main(String[] args) {
BufferedReader br=null;
try {
//这是一个字节流,但是BufferedReader中只能传字符流
FileInputStream fis=new FileInputStream("FileWriterTest.java");
//InputStreamReader是一个转换流,可以把字节流转成字符流
//参数需要一个InputStream,FileInputStream的父类是InputStream
//在这里fis是一个节点流,reader是一个包装流
InputStreamReader reader=new InputStreamReader(fis);
//在这里reader是一个节点流,br是一个包装流
br=new BufferedReader(reader);
String s=null;
while ((s=br.readLine())!=null){
System.out.println(s);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
br.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
//合并写法
br=new BufferedReader(new InputStreamReader(new FileInputStream("FileWriterTest.java")));
BufferedWriter
带缓冲区的字符输出流
public class BufferedWriterTest01 {
public static void main(String[] args) {
BufferedWriter br=null;
try {
//br=new BufferedWriter(new FileWriter("Copy01"));
br=new BufferedWriter(new OutputStreamWriter(new FileOutputStream("Copy01")));
br.write("hello kitty!");
br.flush();
} catch (IOException e) {
e.printStackTrace();
}finally {
if (br != null) {
try {
br.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
BufferedInputStream
带缓冲区的字节输入流
public class BufferedInputStreamTest {
public static void main(String[] args) {
BufferedInputStream bis=null;
try {
bis=new BufferedInputStream(new FileInputStream("src/com/whmc/io/testfile"));
int read=0;
while ((read=bis.read())!=-1){
System.out.println(read);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (bis != null) {
try {
bis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
BufferedOutputStream
带缓冲区的字节输出流
public class BufferedOutputStreamTest {
public static void main(String[] args) {
BufferedOutputStream bos=null;
try {
bos=new BufferedOutputStream(new FileOutputStream("Copy01",true));
byte[] bytes={97,98,99};
bos.write(bytes);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (bos != null) {
try {
bos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
DataOutputStream
数据字节输出流,可以把数据以及它们的数据类型一起保存到文件,该文件不是普通文本文件,记事本打开是乱码,只能用DataInputStream获取数据。而且还需要知道写入规则,按照写的顺序读
public class DataOutputstreamTest {
public static void main(String[] args) {
DataOutputStream dos=null;
try {
dos=new DataOutputStream(new FileOutputStream("data"));
byte b=97;
short s=200;
int i=300;
long l=400l;
float f=1.5f;
double d=3.14;
boolean sex=true;
char c='w';
dos.writeByte(b);
dos.writeShort(s);
dos.writeInt(i);
dos.writeLong(l);
dos.writeFloat(f);
dos.writeDouble(d);
dos.writeBoolean(sex);
dos.writeChar(c);
dos.flush();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
dos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
DataInputStream
数据字节输入流
public class DataInputStreamTest {
public static void main(String[] args) {
DataInputStream dis=null;
try {
dis=new DataInputStream(new FileInputStream("data"));
byte b = dis.readByte();
short s = dis.readShort();
int i = dis.readInt();
long l = dis.readLong();
float f = dis.readFloat();
double d = dis.readDouble();
boolean sex = dis.readBoolean();
char c = dis.readChar();
System.out.println(b);
System.out.println(s);
System.out.println(i);
System.out.println(l);
System.out.println(f);
System.out.println(d);
System.out.println(sex);
System.out.println(c);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}finally {
if (dis != null) {
try {
dis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
PrintStream
标准字节输出流,可以改变文件输出的方向,输出到控制台或者文件
public class PrintStreamTest {
public static void main(String[] args) {
System.out.println("hello world");
//System.out是一个static final修饰的PrintStream类变量
PrintStream printStream=System.out;
printStream.println("hellp kitty");
//标准输出流不用手动close
//标准输出流可以改变输出方向,不输出到控制台,输出到文件
//之前用过的System类的方法和属性
// System.gc();
// System.currentTimeMillis();
// System.arraycopy();
// System.exit(0);
// PrintStream printStream1 = System.out;
//标准输出流不再指向控制台,指向log文件
//PrintStream构造方法需要一个参数OutputStream
try {
PrintStream printStream1=new PrintStream(new FileOutputStream("log"));
//改变输出方向,输出到log文件
System.setOut(printStream1);
System.out.println("helllo world");
System.out.println("hello kitty");
System.out.println("hello zhangsan");
//重新输出到控制台
System.setOut(printStream);
System.out.println("mac");
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
}
可以用于生成日志文件
public class Logger {
public static void log(String msg){
try {
PrintStream out=new PrintStream(new FileOutputStream("logfile.txt",true));
System.setOut(out);
Date now=new Date();
SimpleDateFormat simpleDateFormat=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS");
String s = simpleDateFormat.format(now);
System.out.println(s+":"+msg);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
}
public class LogTest {
public static void main(String[] args) {
Logger.log("用户调用了System.gc(),建议启动垃圾回收器");
Logger.log("用户调用了UserService.dosome()");
Logger.log("用户尝试登录,验证失败");
}
}
logfile.txt 2024-03-25 11:42:58 925:用户尝试登录,验证失败 2024-03-25 11:43:39 646:用户调用了System.gc(),建议启动垃圾回收器 2024-03-25 11:43:39 670:用户调用了UserService.dosome() 2024-03-25 11:43:39 670:用户尝试登录,验证失败
PrintWriter
标准字符输出流
public class PrintWriterTest {
public static void main(String[] args) {
try {
PrintWriter pw=new PrintWriter(new FileWriter("logfile.txt",true));
Date date=new Date();
SimpleDateFormat simpleDateFormat=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS");
String now = simpleDateFormat.format(date);
pw.print(now+":using PrintWriter");
pw.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
}
2024-03-25 12:00:00 609:using PrintWriter