文章目录
- (第一部分)面向对象篇
- (第二部分)IO流
(第一部分)面向对象篇
第7章 类与对象
7.1 面向过程与面向对象思想
面向过程
当我们在解决一个问题时,会按照预先设定的想法和步骤,一步一步去实现,在这里每一步具体的
实现中都需要我们自己去亲自实现和操作。
角色: 执行者
特点:费时间、费精力、结果也不一定完美
面向对象
当我们在解决一个问题时,可以去找具有相对应的功能的事物帮着我们去解决问题,至于这个事物
如何工作,与我们无关,我们只看结果,不关心解决流程。
角色: 指挥者
特点:节省时间,节省精力,结果相对完美
总结
面向过程是面向对象的基础,问题可以自己不解决托给别人解决,别人也可以在再托给别人,但最终,事情必须要处理掉,一旦处理就是面向过程
面向对象和面向过程差异
- 面向对象是一种符合人们思考习惯的思想
- 面向过程中更多的体现是执行者,面向对象中更多的体现是指挥者。
- 面向对象可以将复杂的问题进行简单化,更加贴近真实的社会场景
7.2 类与对象的关系
如何描述一个对象的内容
对象的属性:就是对象的相关参数,可以直接用数据来衡量的一些特征——常量|变量来表示
(成员变量)
对象的行为:就是过将对象的属性进行联动,产生出一系列的动作或行为——函数(成员函
数)
类的定义
类是那些具有相同属性特征和行为的对象们的统称。对象就是该类描述下具体存在的一个事物。
示例1:定义一个圆
//这是一个专门用于程序执行的类,因为里面有主函数
//包含主函数的类 统称为 主类
//当然 实体类中也可以包含主函数
public class Sample {
public static void main(String[] args) {
Circle c1 = new Circle();
System.out.println(c1.radius);
c1.radius = 10;
System.out.println(c1.getArea());
Circle c2 = new Circle();
System.out.println(c2.radius);
System.out.println(c2.getArea());
Circle c3 = new Circle();
c3.type = 2;
c3.setRadius(20);
System.out.println(c3.getPerimeter());
}
}
//这是用于描述圆的一个类
//类似于这种专门描述事物的类 - 实体类
class Circle {
//属性
double radius;
double type = 1;
//行为
public double getArea() {
return Math.PI * radius * radius;
}
public double getPerimeter() {
return 2 * Math.PI * radius;
}
public void setRadius(double newRadius) {
radius = newRadius;
}
}
7.3 封装与private关键字
封装=包装
常见的封装体现:
- 函数
- 类
封装有什么好处?
1.提高了安全性
2.向外界隐藏了一些不需要被外界获知的内容
3.提高了代码的复用性
4.也是面向对象的三大特点之一:封装 继承 多态
private关键字,属于权限关键字 public protected 默认不写 private
private可以作用在对象属性和行为上,外界在创建对象后,则不能访问被private修饰的内容
7.4 局部变量与成员变量
public class Sample {
public static void main(String[] args) {
Person p1 = new Person();
p1.setName("小强");
p1.setAge(10);
p1.speak();
}
}
class Person {
private String name;
private int age;
public void speak() {
System.out.println("我是" + name + ",我今年" + age + "岁");
}
public void setName(String name) {
if (name.equals("旺财")) {
this.name = "哈士奇";
} else {
this.name = name;
}
}
public void setAge(int age) {
if (age < 0) {
this.age = 0;
} else {
this.age = age;
}
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
}
代码执行流程:
- javac 编译Sample.java源代码 生成Sample.class和Person.class两个字节码文件
- 如果java Person ,运行Person字节码文件,则报错,没有主函数不是主类
- 只能java Sample 运行Sample程序
- 将相关的字节码(Sample.class Person.class)文件加载进JVM中内存下的方法区
- 在方法区中Sample字节码所在的区域里,找主函数,将主函数的栈帧加载进栈内存开始运行
- 开始执行主函数的第一句代码,创建Person对象
- 在堆内存中开辟一个空间并分配地址,在该空间中创建成员变量并默认初始化
- 在主函数空间中创建局部变量p1,并将该对象的地址传给p1
- 接着执行主函数第二句代码,调用p1对象的setName方法
- 从方法区中的Person里,将setName函数栈帧加载进栈,主函数暂停运行
- setName进栈后,创建局部变量name(形参),并将实参“小强”这个字符串在字符串常量池中的地址赋予name
- 因为setName成员函数只有一份在方法区中Person所属区间里,之后可以被多个同类对象调用,为了区分到底是哪个对象调用的该方法,所以在每一个成员函数中,都会有一个隐藏的关键字数据 this ,this相当于一个变量来存储当前对象的地址。(当前对象的引用)
- 执行setName中的内容,如果数据没有问题的话,就将局部变量的值赋值个当前对象的成员变量
- setName函数执行最后一行隐藏的return,表示函数结束并弹栈
- 主函数成为当前栈顶,继续执行
- 执行p1调用setAge函数,从方法区中Person所属空间里找setAge这一段代码,将该函数栈帧加载进栈内存成为新的栈顶,则主函数暂停,该函数运行。先创建形参age的局部变量,接收实参传来的值10,为了区分对象的调用关系,自带this关键字数据,this存的还是p1的地址,如果age没有问题,则将10传给this所指向的对象中age这个成员变量。setAge执行最后一行隐藏的return,表示函数结束并弹栈
- 主函数称为新的栈顶继续执行,调用p1的speak函数进栈
- 在方法区中Person字节码所属空间里读取speak代码,将该栈帧加载进栈内存中,主函数暂停,该函数执行,无形参只能表示没有形参的局部变量,但是在函数内部也可以创建其他的局部变量,并且有this关键数据存的是p1的地址,然后去打印name和age,由于speak空间中已经没有其他名为name或age的局部变量,所以找不到,接着找this对象中的数据,找到了则打印。直至函数结束并弹栈
- 主函数又称为栈顶,也没有代码了,执行隐藏的return,主函数弹栈,表示程序结束。
局部变量和成员变量的区别
局部变量 | 成员变量 | |
---|---|---|
生命周期 | 随着函数的进栈而创建,随着函数的出栈而消失 | 随着对象的创建而创建,随着对象的消亡而消失 |
存储位置 | 在栈内存中函数所属空间里 | 在堆内存中对象所属空间里 |
定义位置 | 局部变量在函数中定义 | 成员函数在类中,函数外定义 |
初始化 | 必须初始化之后再调用 | 有默认初始化 |
7.5 构造函数
构造函数:构造函数主要是在创建对象的时候执行的函数,在该函数中也可对成员变量进行一些操作。
构造函数的格式
权限修饰符 类名(参数列表) {
构造函数的代码块
}
- 构造函数没有返回值
- 构造函数的名称必须是类名
- 参数列表可选的,构造函数是可以重载的
- 虽然构造函数没有返回值,还是存在return关键字的
- 当我们的类中没有定义任何构造函数时,会有一个默认隐藏的无参构造函数存在
- 构造函数和成员函数一样,为了区分对象的调用,构造函数自带this关键字数据
构造函数需要注意的问题
1.如果一旦定义其他参数列表的构造函数的话,这个隐藏的无参构造函数就会消失,所建议手写出来
2.构造函数只有在创建对象的时候执行,当对象创建完毕之后,该对象的构造函数则不能执行
3.成员函数只有在对象创建之后才能执行
4.成员函数能否直接调用构造函数?不能够的,报找不到符号错误 会误认为是同名的成员函数
5.构造函数能否直接调用成员函数呢?能够,但是 这些成员函数一般是构造函数的部分代码片段被切割
出来了而已,从语意上而言,不属于对象的特有行为(也有特例),所以这些函数长得样子就是成员函数
的样子,但没有必要向外界提供访问,所以加上private
6.构造函数能否直接调用构造函数呢?可以,但是必须通过 this(参数列表) ,需要注意的是,构造
函数可以单向调用其他构造函数,但坚决不能出现回调。
7.构造函数是在创建对象的时候执行的,可以在期间对成员变量进行初始化,问:setXXX还需要不?
看需求,如果后期成员变量需要修改,则提供setXXX修改器
public class Sample {
public static void main(String[] args) {
Person p = new Person();//new 构造函数;
p.setName("旺财");
p.setAge(10);
p.speak();
Person p2 = new Person();//new 构造函数;
p2.setName("小强");
p2.setAge(20);
p2.speak();
Person p3 = new Person("如花",40);
p3.speak();
p3.test();
//p3.part();
Car car1 = new Car();
Car car2 = new Car(4);
Car car3 = new Car(4,"红色");
Car car4 = new Car(8,"武士黑",20);
car1.run();
car2.run();
car3.run();
car4.run();
}
}
class Car {
private int wheel = 4;
private String color;
private int weight;
public Car(){
}
public Car(int wheel) {
this(wheel,null,0);
}
public Car(int wheel,String color) {
this(wheel,color,0);
}
public Car(int wheel,String color,int weight) {
this.wheel = wheel;
this.color = color;
this.weight = weight;
//this();//递归构造器调用
}
public void run() {
System.out.println(wheel + ":" + color + ":" + weight);
}
//这个不是重载 Car(int)已经存在了(wheel)
/*
public Car(int weight) {
}
*/
}
class Person {
private String name;
private int age;
//这就是隐藏的构造函数
public Person() {
System.out.println("一个Person创建出来了!");
part();
part();
part();
part();
}
//语意
private void part() {
System.out.println("100行代码");
}
public Person(String name,int age) {
System.out.println("一个Person创建出来了!");
this.name = name;
this.age = age;
}
public void test() {
Person();//实际上这一段代码并不表示调用无参构造函数
//而表示去调用名字为Person的成员函数!
}
public void Person() {
System.out.println("没想到吧!");
}
public void speak() {
System.out.println(name + ":" + age);
}
public void setName(String name) {
this.name = name;
}
public void setAge(int age) {
this.age = age;
}
}
关于成员变量初始化的问题
成员变量的初始化经历了三个步骤:默认初始化(大家默认都是0值),显式初始化(大家的值都一样),针对性初始化(大家的值可选)
7.6 对象的创建流程及内存图解
示例:定义一个栈
public class Sample{
public static void main(String[] args) {
Stack stack = new Stack();
stack.pop();
System.out.println(stack);
for(int i=1; i<=10;i++){
stack.push(i);
}
System.out.println(stack);
stack.pop();
System.out.println(stack);
System.out.println(stack.peek());
stack.clear();
System.out.println(stack);
System.out.println(stack.peek());
}
}
class Stack{
private int[] data;
private int top = -1;
private static int capacity = 10;
public Stack() {
this(capacity);
}
public Stack(int capacity) {
data = new int[capacity];
}
public void push(int e){
if(size() == data.length) {
reSize(data.length);
}
data[++top] = e;
}
public void reSize(int length){
int[] arr = new int[length * 2];
for(int i=0; i<= top; i++) {
arr[i] = data[i];
}
data = arr;
}
public int pop(){
if(isEmpty()){
System.out.println(">>>栈为空,弹不出元素了!");
return -1;
}
int e = data[top];
top--;
if(data.length > capacity && size() == data.length / 4) {
reSize(data.length / 2);
}
return e;
}
public void clear(){
top = -1;
data = new int[10];
}
public int peek(){
if(isEmpty()){
System.out.println(">>>栈为空,无栈顶元素!");
return -1;
}
return data[top];
}
public boolean isEmpty(){
return top == -1;
}
public int size() {
return top + 1;
}
public String toString() {
if(isEmpty()){
return "[]";
}
String s = "[";
for(int i=0; i <= top; i++) {
if(i == top){
s += data[i] + "]";
}else{
s += data[i] + ", ";
}
}
return s;
}
}
示例:模拟吃鸡
public class Sample02{
public static void main(String[] args) {
Player p1 = new Player("老王",100);
Player p2 = new Player("老李",100);
p1.shootEnemy(p2);
Gun gun = new Gun();
p1.holdGun(gun);
p1.shootEnemy(p2);
Clip clip = new Clip();
for (int i = 1; i <= 30; i++) {
clip.pushBullet(new Bullet());
}
p1.loadClip(clip);
for (int i = 1; i <= 30; i++) {
p1.shootEnemy(p2);
}
p1.shootEnemy(p2);
}
}
//人
class Player{
//人物姓名
private String name;
//人物血值
private int blood;
//人物的枪
private Gun gun;
public Player() {}
//人物初始化
public Player(String name,int blood){
this(name,blood,null);
}
//人物初始化
public Player(String name,int blood,Gun gun){
this.name = name;
this.blood = blood;
this.gun = gun;
}
//设置枪
public void holdGun(Gun gun) {
this.gun = gun;
}
//攻击敌人
public void shootEnemy(Player enemy) {
if(gun == null) {
System.out.println(">>>玩家信息:没有枪,开P");
}else{
System.out.printf(">>>玩家信息:%s向%s开了一枪\n",name,enemy.name);
gun.shootEnemy(enemy);
}
}
//上弹夹
public void loadClip(Clip clip) {
if(gun == null){
System.out.println(">>>玩家信息:没抢,装不了弹夹");
}else{
gun.loadClip(clip);
}
}
//玩家受到攻击
public void damage(int hurt) {
//血量为0,攻击不了
if(blood == 0) {
System.out.println(">>>玩家信息:" + name + "已经成盒,请勿鞭尸");
}else{
//否则扣血
blood -= hurt;
if(blood > 0){
System.out.println(">>>玩家信息:" + name + "掉血" + hurt + ",剩余" +blood);
}else{
//扣血小于0后,重新赋血值,输出信息
blood = 0;
System.out.println(">>>玩家信息:" + name + "已经成盒");
}
}
}
}
//枪
class Gun{
private Clip clip;
public Gun() {
this(null);
}
//初始化弹夹
public Gun(Clip clip) {
this.clip = clip;
}
//上弹夹
public void loadClip(Clip clip) {
this.clip = clip;
}
//攻击敌人
public void shootEnemy(Player enemy) {
//判断枪是否有弹夹
if(clip == null) {
System.out.println(">>>枪信息:没有弹夹,开了个空枪");
return;
}
//弹夹弹出一颗子弹
Bullet bullet = clip.popBullet();
//弹出子弹为空,则弹夹没有子弹
if(bullet == null) {
System.out.println(">>>枪信息:弹夹没子弹 开了个空枪");
return;
}else{
bullet.hitEnemy(enemy);
}
}
}
//弹夹
class Clip{
//弹夹的容量
private int capacity = 30;
//弹夹剩余的子弹
private int surplus = 0;
//弹夹的子弹容器
private Bullet[] magazine;
public Clip(){
this(30);
}
//初始化子弹容器和弹夹的容量
public Clip(int capacity) {
this.capacity = capacity;
magazine = new Bullet[capacity];
}
//装入一颗子弹
public void pushBullet(Bullet bullet) {
//判断弹夹中子弹是否已经装满
if(surplus == capacity) {
System.out.println(">>>弹夹信息:弹夹已满,无法装入子弹");
return;
}else{
magazine[surplus++] = bullet;
showClip();
}
}
//弹出一颗子弹
public Bullet popBullet() {
//判断弹夹中是否有子弹
if(surplus == 0){
System.out.println(">>>弹夹信息:弹夹为空,无法弹出子弹!");
return null;
}else {
//从子弹容器中取出一颗子弹
Bullet bullet = magazine[surplus-1];
//剩余子弹--
surplus--;
showClip();
return bullet;
}
}
public void showClip() {
System.out.printf(">>>弹夹信息:%d/%d\n",surplus,capacity);
}
}
//子弹
class Bullet{
//子弹的伤害
private int hurt;
public Bullet() {}
//为子弹赋特殊伤害
public Bullet(int hurt) {
this.hurt = hurt;
}
//子弹伤害敌人
public void hitEnemy(Player enemy) {
enemy.damage(hurt);
}
}
7.7 static关键字
静态关键字
主要用于:修饰成员变量(对象的特有属性)和成员函数,变为静态变量和静态函数
静态变量的特点:同类下多个对象之间的共有属性
静态变量的定义时机:在同一类下,多个对象之间有相同的属性和值,那么就可以将该属性和值从成员变量变为静态变量,目的就是为了节省空间。
静态函数的定义时机:当一个成员函数不访问成员时,即可定义为静态函数!
- 静态函数一旦定义出来,可以直接用类名去调用,当然也可以通过创建对象来去调用静态函数!
- 静态优先于对象存在,且在同一类中,静态无法访问非静态(成员),非静态是可以访问静态
- 静态函数中不存在this()
1.当通过对象去调用一个属性时,先找成员,再找静态,最后找父类
2.如果从成员函数中去调用一个属性时,先找局部,再找成员,再找静态,最后找父类
好处:
- 节省堆内存中的空间
- 可以不用费力气去创建对象来调用功能
- 可以对类进行一些初始操作(结合代码块来做)
import java.util.Scanner;
public class Demo {
public static void main(String[] args) {
Chinese c1 = new Chinese();
Chinese c2 = new Chinese();
Chinese c3 = new Chinese();
System.out.println(c1.country);
System.out.println(Chinese.country);
c1.show();
Chinese.show();
/*
Chinese.test();
*/
}
}
class Chinese {
String name;
int age;
static String country;
//静态代码块
static {
country = "China";
System.out.println("init....");
}
Chinese() {
System.out.println("Chinese....");
}
public void test(){
int num = 10;
System.out.println(num + name + country);
}
public static void show() {
/*
//无法从静态上下文中引用非静态 方法 test()
test();
//无法从静态上下文中引用非静态 变量 name
System.out.println("show...." + name);
*/
}
}
7.8 静态变量与成员变量
静态变量 | 成员变量 | |
---|---|---|
生命周期 | 随着类的加载而存在,随着程序的结束而消失 | 随着对象的创建而创建,随着对象的消亡而消失 |
存储位置 | 存储在静态方法区中对应的字节码空间里 | 在堆内存中对象所属空间里 |
所属 | 静态变量属于类的,称之为是类的属性,或者叫对象的共有属性 | 成员变量属于对象的,称之为是对象的特有属性 |
调用方式 | 静态变量在外界可以通过对象调用,也可以通过类来调用,内部的话静态函数/成员函数可以调用静态变量 | 成员变量在外界必须通过创建对象来调用,内部的话成员函数可以直接调用成员变量,但是静态函数不能直接调用成员变量,如果非要在静态函数中调用成员的话,只能创建对象,通过对象来调用 |
public class Demo {
public static void main(String[] args) {
//StackOverFlowError 栈内存溢出
A a = new A();
System.out.println(a == a.a);
System.out.println(a.a == a.a.a);
}
}
class A {
//OutOfMemoryError 堆内存溢出
int[][] arr = new int[1024][1024];
A a = new A();
}
public class Demo {
public static void main(String[] args) {
A a = new A();
System.out.println(a == a.a);
System.out.println(a.a == a.a.a);
}
}
class A {
static A a = new A();
}
编程练习题
public class Demo106{
public static void main(String[] args){
Rectangle c1 = new Rectangle();
System.out.println(c1.getArea());
System.out.println(c1.getPerimeter());
Rectangle c2 = new Rectangle(3,6);
System.out.println(c2.getArea());
System.out.println(c2.getPerimeter());
}
}
class Rectangle {
private double width = 1;
private double height = 1;
//创建默认矩形的无参构造方法。
public Rectangle(){
}
public Rectangle(double width,double height){
this.width = width;
this.height = height;
}
//面积
public double getArea(){
return height * width;
}
//周长
public double getPerimeter(){
return 2 * (height + width);
}
}
public class Demo108{
public static void main(String[] args){
Fan f1 = new Fan();
System.out.println(f1.toString());
f1.setSpeed(Fan.FAST);
f1.setOn(true);
f1.setColor("gruue");
f1.setRadius(10);
System.out.println(f1.toString());
}
}
class Fan{
//三个名为SLOW、MEDIUM和 FAST而值为1、2和3的常量,表示风扇的速度。
public static final int SLOW = 1;
public static final int MEDIUM = 2;
public static final int FAST = 3;
//一个名为speed的int类型私有数据域,表示风扇的速度(默认值为SLOW)。
private int speed = SLOW;
//一个名为on 的 boolean类型私有数据域,表示风扇是否打开(默认值为fa1se)
private boolean on = false;
//一个名为radius 的double类型私有数据域,表示风扇的半径(默认值为5)。
private double radius = 5;
//一个名为color的 string类型数据域,表示风扇的颜色(默认值为blue)。
private String color = "blue";
//无参构造函数
public Fan(){}
//toString
public String toString(){
if(on){
return speed + color + radius;
}else{
return "fan is off " + color + radius;
}
}
//访问器
public int getSpeed(){
return speed;
}
public boolean isOn(){
return on;
}
public double getRadius(){
return radius;
}
public String getColor(){
return color;
}
//修改器
public void setSpeed(int speed){
this.speed = speed;
}
public void setOn(boolean on){
this.on = on;
}
public void setRadius(double radius){
this.radius = radius;
}
public void setColor(String color){
this.color = color;
}
}
public class Demo109{
public static void main(String[] args){
RegularPolygon r1 = new RegularPolygon();
System.out.println(r1.getPerimeter());
System.out.println(r1.getArea());
RegularPolygon r2 = new RegularPolygon(6,4);
System.out.println(r2.getPerimeter());
System.out.println(r2.getArea());
RegularPolygon r3 = new RegularPolygon(10,4,5.6,7.8);
System.out.println(r3.getPerimeter());
System.out.println(r3.getArea());
}
}
class RegularPolygon{
private int n = 3;
private double side = 1;
private double x = 0;
private double y = 0;
//无参构造函数
public RegularPolygon(){}
//一个能创建带指定边数和边长度、中心在(0,0)的正多边形的构造方法
public RegularPolygon(int n,double side){
this(n,side,0,0);
/*
this.n = n;
this.side =side;
*/
}
//一个能创建带指定边数和边长度、中心在(x,y)的正多边形的构造方法。
public RegularPolygon(int n,double side,double x,double y){
this.n = n;
this.side =side;
this.x = x;
this.y = y;
}
//访问器
public int getN(){
return n;
}
public double getSide(){
return side;
}
public double getX(){
return x;
}
public double getY(){
return y;
}
//修改器
public void setN(int n){
this.n = n;
}
public void setSide(double side){
this.side = side;
}
public void setX(){
this.x = x;
}
public void setY(){
this.y = y;
}
//返回多边形周长的方法
public double getPerimeter(){
return n *side;
}
//返回多边形面积的方法
public double getArea(){
return n * side *side / (4 * Math.tan(Math.PI / n));
}
}
public class Demo112{
public static void main(String[] args){
Time t1 = new Time();
System.out.println(t1.toString());
Time t2 = new Time(555550000);
System.out.println(t2.toString());
Time t3 = new Time(9,24,31);
System.out.println(t3.toString());
}
}
class Time{
//表示时间的数据域hour、 minute和second。
private long hour;
private long minute;
private long second;
//一个以当前时间创建Time对象的无参构造方法(数据域的值表示当前时间)。
public Time(){
this(System.currentTimeMillis());
/*
long millis = System.currentTimeMillis();
hour = cacluHour(millis);
minute = cacluMinute(millis);
second = cacluScound(millis);
*/
}
//一个构造Time对象的构造方法,这个对象有一个特定的时间值,
//这个值是以毫秒表示的、从1970年1月1日午夜开始到现在流逝的时间段(数据域的值表示这个时间)。
public Time(long millis){
hour = cacluHour(millis);
minute = cacluMinute(millis);
second = cacluScound(millis);
}
//一个构造带特定的小时、分钟和秒的Time对象的构造方法。
public Time(long hour,long minute,long second){
this.hour = hour;
this.minute = minute;
this.second = second;
}
//get访问器
public long getHour(){
return hour;
}
public long getMinute(){
return minute;
}
public long getSecond(){
return second;
}
//一个名为setTime(long elapseTime)的方法使用流逝的时间给对象设置一个新时间。
public void setTmie(long elapseTime){
hour = cacluHour(elapseTime);
minute = cacluMinute(elapseTime);
second = cacluScound(elapseTime);
}
public String toString(){
return hour + ":" + minute + ":" + second;
}
private long cacluHour(long millis){
return millis / 1000 / 60 /60 % 24;
}
private long cacluMinute(long millis){
return millis / 1000 / 60 % 60;
}
private long cacluScound(long millis){
return millis / 1000 % 60;
}
}
public class Demo113{
public static void main(String[] args){
//自测
MyInteger m1 = new MyInteger(3);
System.out.println(m1.isEven());
System.out.println(m1.isOdd());
System.out.println(m1.isPrime());
//用工具测
MyInteger m2 = new MyInteger(8);
System.out.println(MyInteger.isEven(m2));
MyInteger m3 = new MyInteger(15);
System.out.println(MyInteger.isPrime(m3));
System.out.println(m2.equals(m3));
System.out.println(MyInteger.parseInt("1234") + 2);
}
}
class MyInteger{
//一个名为value的int型数据域,存储这个对象表示的int值。
private int value;
//一个为指定的int值创建MyInteger对象的构造方法。
public MyInteger(int value){
this.value = value;
}
//一个返回int值的get方法。
public int get(){
return value;
}
//如果值分别为偶数、奇数或素数,那么isEven()、is0dd()和isPrime()方法都会返回true。
public boolean isEven(){
return value % 2 == 0;
}
public boolean isOdd(){
return value %2 ==1;
}
public boolean isPrime(){
for(int i = 2;i <= value / 2;i++){
if(value % 2 == 0){
return false;
}
}
return true;
}
//如果指定值分别为偶数、奇数或素数,那么相应的静态方法isEven(int)、is0dd(int)和isPrime(int)会返回true。
public static boolean isEven(MyInteger Integer){
return Integer.get() % 2 == 0;
}
public static boolean isOdd(MyInteger Integer){
return Integer.get() %2 ==1;
}
public static boolean isPrime(MyInteger Integer){
for(int i = 2;i <= Integer.get() / 2;i++){
if(Integer.get() % 2 == 0){
return false;
}
}
return true;
}
//如果该对象的值与指定的值相等,那么equals(int)和equals(MyInteger)方法返回true。
public boolean equals(int num){
return value == num;
}
public boolean equals(MyInteger Integer){
return value == Integer.get();
}
//"1234"
public static int parseInt(String str){
int result = 0;
for(int i = 0;i <= str.length() - 1;i++){
int num = str.charAt(i) - '0';
result = result *10 + num ;
}
return result;
}
}
public class Demo114{
public static void main(String[] args){
MyPoint p1 = new MyPoint(2,2);
MyPoint p2 = new MyPoint(4,-2);
System.out.println(p1.distance(p2));
System.out.println(p2.distance(1,1));
}
}
class MyPoint{
//两个带get方法的数据域×和y分别表示它们的坐标。
private double x;
private double y;
public double getX(){
return x;
}
public double getY(){
return y;
}
//无参构造函数
public MyPoint(){
x = 0;
y = 0;
}
//特定点的构造函数
public MyPoint(double x,double y){
this.x = x;
this.y = y;
}
//一个名为distance的方法,返回从该点到MyPoint类型的指定点之间的距离。
public double distance(MyPoint point){
return Math.hypot(x - point.x , y - point.y);
}
//一个名为distance的方法,返回从该点到指定x和y坐标的指定点之间的距离。
public double distance(double x,double y){
return Math.hypot(this.x - x , this.y - y);
}
}
public class Demo115{
public static void main(String[] args){
Queue q1 = new Queue();
for(int i = 0; i <= 20 ;i++){
q1.enqueue(i);
}
System.out.println(q1.toString());
}
}
class Queue{
//有效元素的个数
private int size;
//队列的默认容量
private int capacity = 8;
//创建容器,当element.length == size,表示已满
private int[] element;
public Queue(){
element = new int[capacity];
size = 0;
}
//入队
public void enqueue(int v){
if(size == element.length){
//队列已满需扩容
resize(element.length * 2);
}
element[size++] = v;
}
//出队
public void dequeue(){
if(size == 0){
return -1;
//出错
}
int ret = element[0];
for(int i = 1;i < size;i++){
element[i - 1] = element[i];
}
size--;
if(size <= element.length / 4 && element.length > capacity){
//缩容
resize(element.length / 2);
}
return ret;
}
public boolean isEmpty(){
return size == 0;
}
public int size(){
return size;
}
//缩容和扩容函数
private void resize(int newlength){
int[] newlement = new int [newlength];
for(int i = 0; i < size; i++){
newlement[i] = element[i];
}
element = newlement;
}
//返回队列中的所有元素(封装对象的 信息,用于打印)
public String toString(){
String s = "[";
if(size == 0){
s += "]";
}else{
for(int i = 0 ;i < size ;i++){
if(i == size - 1){
s = s + element[i] + "]";
}else{
s = s + element[i] + ",";
}
}
}
return s;
}
}
public class Demo117 {
public static void main(String[] args) {
char[] ch = {'a','b','c'};
MyString s1 = new MyString(ch);
MyString s2 = new MyString("abg");
System.out.println(s1.compareTo(s2));
MyString s3 = new MyString("PPviuiNN");
s3.toLowerCase().show();
s1.toUpperCase().show();
System.out.println(s2.compareToIgnoreCase(s3));
s3.concat(s1).show();
System.out.println(s1.contains(s2));
MyString s4 = new MyString("xxx.mp4");
System.out.println(s4.endsWith(new MyString(".mp4")));
System.out.println(s4.equals(new MyString("xxx.mp")));
System.out.println(s4.equalsIgnoreCase(new MyString("xxx.MP4")));
System.out.println(s4.indexOf('m'));
System.out.println(s4.lastIndexOf('x'));
System.out.println(s3.indexOf(new MyString("iui")));
System.out.println(s3.lastIndexOf(new MyString("iui")));
s3.replace('P','a').show();
System.out.println(s3.startWith(new MyString("PP")));
s3.substring(2,5).show();
}
}
class MyString {
//定义一个字符数组来存放字符串,不能在其中修改元素
private char[] data;
public MyString(char[] chars) {
//将外部的字符数组,传递给内部的字符数组
data = new char[chars.length];
for(int i=0; i<chars.length; i++) {
data[i] = chars[i];
}
}
//将外部字符串传递给内部的data
public MyString(String s) {
data = new char[s.length()];
for(int i=0; i<s.length();i++){
data[i] = s.charAt(i);
}
}
//将MyString的类型的字符串赋值到data中
public MyString(MyString str) {
data = new char[str.length()];
for(int i=0; i<str.length();i++){
data [i] = str.charAt(i);
}
}
//根据字符索引返回字符
public char charAt(int index) {
return data[index];
}
//返回字符数组的长度
public int length() {
return data.length;
}
//截取字符串
public MyString substring(int beginIndex,int endIndex){
char[] ch = new char[endIndex-beginIndex];
int index = 0;
for(int i=beginIndex;i<endIndex;i++){
ch[index++] = data[i];
}
return new MyString(ch);
}
//判断是否以字符开头
public boolean startWith(MyString s){
//定义两个指针,分别判断两个字符串的首字母是否相等,相等就继续判断
//判断到有一个字符串溢出为止,有溢出就返回true,否则就为false
int i = 0;
int j = 0;
while(true){
if(charAt(i) == s.charAt(j)){
i++;
j++;
if(j >= s.length()){
return true;
}
}else{
return false;
}
}
}
//替换字符串中的字符
public MyString replace(char oldChar,char newChar){
char[] chars = new char[length()];
for(int i=0;i<length();i++){
if(charAt(i) == oldChar){
chars[i] = newChar;
}else{
chars[i] = data[i];
}
}
return new MyString(chars);
}
//返回第一个字符相等的角标
public int indexOf(char c){
for(int i=0;i<length();i++){
if(charAt(i) == c){
return i;
}
}
return -1;
}
//返回最后一个字符相等的角标
public int lastIndexOf(char c){
for(int i=length()-1;i>=0;i--){
if(charAt(i) == c){
return i;
}
}
return -1;
}
//返回字符串的第一个角标
public int indexOf(MyString s){
//将两个数组变为局部变量,方便比较
char[] ch1 = data;
char[] ch2 = s.data;
//循环数组,每次比较两个字符串的首字母是否相等,相等的就遍历第二个字符串
//并与接下去的第一个字符串的字符比较,只要有一个不相等就返回-1,否则就返回i对应的角标
for(int i=0;i<length() - ch2.length;i++){
if(ch1[i] == ch2[0]){
int j = i+1;
for(int l=1;l<ch2.length;l++,j++){
if(ch1[j] != ch2[l]){
return -1;
}
}
return i;
}
}
return -1;
}
//返回字符串相等的最后的一个角标
public int lastIndexOf(MyString s){
//将两个数组变为局部变量,方便比较
char[] ch1 = data;
char[] ch2 = s.data;
//循环数组,每次比较两个字符串的首字母是否相等,相等的就遍历第二个字符串
//并与接下去的第一个字符串的字符比较,只要有一个不相等就返回-1,否则就返回i对应的角标
for(int i = ch1.length-1;i>=ch2.length-1;i--){
if(ch1[i] == ch2[ch2.length-1]){
int j = i -1;
for(int l=ch2.length-2;l>=0;l--,j--){
if(ch1[j] != ch2[l]){
return -1;
}
}
return j+1;
}
}
return -1;
}
//判断两个字符串是否相等忽略大小写
public boolean equalsIgnoreCase(MyString s){
return compareToIgnoreCase(s) == 0;
}
//判断两个字符串是否相等
public boolean equals(MyString s){
return compareTo(s) == 0;
}
//判断字符串是否以xx结尾
public boolean endsWith(MyString s) {
//都从字符串的最后一个字符开始遍历
//判断两个字符是否相等,不相等返回false,直到有一个字符遍历完了后就返回true
int i = length() - 1;
int l = s.length() - 1;
while(true){
if(charAt(i) == s.charAt(l)){
i--;
l--;
if(l<0){
return true;
}
}else{
return false;
}
}
}
//判断一个字符串是否包含另一个字符串
public boolean contains(MyString s){
//将两个数组变为局部变量,方便比较
char[] ch1 = data;
char[] ch2 = s.data;
//循环数组,每次比较两个字符串的首字母是否相等,相等的就遍历第二个字符串
//并与接下去的第一个字符串的字符比较,只要有一个不相等就返回false,否则就返回true
for(int i=0;i<length() - ch2.length;i++){
if(ch1[i] == ch2[0]){
int j = i+1;
for(int l=1;l<ch2.length;l++,j++){
if(ch1[j] != ch2[l]){
return false;
}
}
return true;
}
}
return false;
}
//连接两个字符串
public MyString concat(MyString s) {
//创建一个行的数组,长度为两个字符串的总和
//分别遍历两个数组,将其内容复制到新的数组中返回即可
char[] chars = new char[length() + s.length()];
int index =0;
for(int i=0;i<length();i++){
chars[index++] = data[i];
}
for(int i=0; i<s.length();i++){
chars[index++] = s.charAt(i);
}
return new MyString(chars);
}
//比较两个字符串的大小,忽略大小写
public int compareToIgnoreCase(MyString s) {
//将两个字符串都转为小写的字符串,在比较大小
MyString s1 = toLowerCase();
MyString s2 = s.toLowerCase();
return s1.compareTo(s2);
}
//将字符串中大写的字母转换为小写的字母
public MyString toLowerCase() {
//创建一个新的字符数组来存放转变后的字符
char[] chars = new char[length()];
for(int i=0; i<length();i++){
char ch = data[i];
if(isUpperLetter(ch)){
//将大写的字母转换为小写的字母
chars[i] = (char) (ch + 32);
}else{
//不是字母就直接赋值
chars[i] = ch;
}
}
return new MyString(chars);
}
//转换为大写字母
public MyString toUpperCase() {
char[] chars = new char[length()];
for(int i=0; i<length();i++){
char ch = data[i];
if(isLowerLetter(ch)){
chars[i] = (char) (ch - 32);
}else{
chars[i] = ch;
}
}
return new MyString(chars);
}
//判断是否为小写的字母
public boolean isLowerLetter(char ch){
return ch >= 'a' && ch <= 'z';
}
//判断是否为大写的字母
public boolean isUpperLetter(char ch){
return ch >= 'A' && ch <= 'Z';
}
//比较两个字符串的大小
public int compareTo(MyString s){
int i = 0;
int j = 0;
//定义左右指针,如果比较两个字符,如果相等就增加,有一个不等就返回两字符的差值,
//如果有一个字符已经遍历过了最大的角标就返回两个字符串长度的差值
while(true) {
if(charAt(i) == s.charAt(j)){
i++;
j++;
if(i == length() && j == s.length()){
return 0;
}
if(i < length() && j>=s.length() || i >= length() && j < s.length()){
return length() - s.length();
}
}else{
return charAt(i) - s.charAt(j);
}
}
}
//打印字符串
public void show() {
for(int i=0;i<length();i++){
System.out.print(data[i]);
}
System.out.println();
}
}
第8章 继承
继承的概述
多个类中存在相同属性和行为时,将这些内容抽取到单独一个类中,那么多个类无需再定义这些属性和行为,只要继承那一个类即可。其中,多个类可以称为子类,单独那一个类称为父类、超类(superclass)或者基类。父类与子类之间用extends关键字来标明
class Student extends Person{
void study() {
System.out.println("study....");
}
}
class Worker extends Person{
void work() {
System.out.println("work....");
}
}
class Teacher extends Person {
void teach() {
System.out.println("teach....");
}
}
class Person {
String name;
int age;
}
优点
- 继承的出现提高了代码的复用性
- 继承的出现让类与类之间产生关系,也为我们后面多态提供了前提
单继承与多继承
Java中 类与类之间只能支持单继承, 接口与接口之间可以支持多继承
class Demo extends Demo1{}
class Demo extends Demo1,Demo2{}//Error
需要注意的一些问题
- 继承和传统的理解稍微有一些不同的,子类并不是或是父类的一个子集!实际上,一个子类通
常比它的父类包含更多的信息和方法。(子类更多的是父类的一种升级和延伸) - 父类中的一些私有内容,子类是获取不到的。如果父类对其自身的私有内容设置了公共的访问
器和修改器的话,子类可以通过该访问器和修改器来获取父类的私有内容。 - 不要为了获取某一个特殊的属性或行为,而去乱认爸爸
- 说来说去,总之就是想表达一个观点,在设计类和其继承关系时,一定要符合社会常识认知伦理问题
子父类中,成员变量的特点
- 子类没有,父类有且非私有,子类对象能获取num
- 子类没有,父类有且私有,子类对象不能获取num
- 子类有,父类有且非私有,子类对象获取的是子类的num 不存在重写的概念
- 子类有,父类没有, 子类对象获取的是子类的num
public class Sample {
public static void main(String[] args) {
Zi zi = new Zi();
System.out.println(zi.num);
zi.show();
}
}
class Fu {
int num = 10;
}
//父类 超类 基类
class Zi extends Fu {
int num = 20;
void show() {
int num = 30;
System.out.println(num + "," + this.num + "," + super.num);
}
}
- 如果子类中,成员变量 和 父类变量 和 局部变量 重名时,以上就行。这里面有一个特殊的 super
this表示的是当前对象,存的是当前对象在堆内存中的地址
super不表示父类的对象,因为在此我们并没有去创建父类的对象!super仅仅表示父类空间,
并没有创建父类的对象!
子父类中,构造函数的特点
当创建子类对象时,在子类的构造函数执行之前,父类的构造函数先执行,虽然父类的构造函数执行但不代表父类对象的创建。
1.每一个类中的构造函数第一句如果不是 this() 调用本类中其他构造函数的话,默认第一句是隐藏的 super()
2.是因为在创建子类对象的时候,需要父类为继承给子类的一些数据进行初始化。
public class Sample {
public static void main(String[] args) {
Zi zi = new Zi();
}
}
class Fu {
int num;
Fu() {
System.out.println("Fu show..." + num);
num = 4;
}
}
class Zi extends Fu {
Zi() {
super(); //调用父类的无参构造函数 默认是隐藏的
//父类的构造函数一旦执行完毕 则紧接着执行子类成员变量的显示初始化
System.out.println("Zi show..." + num);
}
}
/*
结果:
Fu show...0
Zi show...4
*/
this()与super()是否冲突
- 如果子类的多个构造函数们之间没有调用关系,则每一个子类的构造函数第一句都是super(),即子类的多个构造函数都是先调用父类的构造函数对父类的数据进行初始化。
- 子类中有 多个构造函数,且存在构造函数之间的调用关系,即构造函数的第一句为this(参数列表),在使用该构造函数创建对象时会调用该类的其他构造函数来调用父类的构造函数。
public class Sample {
public static void main(String[] args) {
Zi zi1 = new Zi();
Zi zi2 = new Zi(1);
Zi zi3 = new Zi(1,2);
}
}
class Fu {
int num;
Fu() {
System.out.println("Fu show...");
}
}
class Zi extends Fu {
Zi() {
this(1);
System.out.println("Zi show ... 0");
}
Zi(int a) {
this(1,2);
System.out.println("Zi show ... 1");
}
Zi(int a , int b) {
super();
System.out.println("Zi show ... 2");
}
}
/*
结果:
Fu show...
Zi show ... 2
Zi show ... 1
Zi show ... 0
Fu show...
Zi show ... 2
Zi show ... 1
Fu show...
Zi show ... 2
*/
小结:
在构造函数中,第一句要么是this(),要么是super()
不存在每一个构造函数第一句都是this(),否则递归调用
存在每一个构造函数第一句都是super(),构造函数之间不调用
this()与super()本身不冲突的,如果构造函数之间有调用关系,那么最后一个被调用的构造函数就不能再回调,那么其第一句就不能是this(),只能是super()
如果父类中,没有无参构造函数的存在,只有有参数的构造函数的话,那么子类中默认的super() 就调用不到父类无参构造函数!引发错误!
public class Sample {
public static void main(String[] args) {
Zi zi1 = new Zi();
Zi zi2 = new Zi(3);
Zi zi3 = new Zi(5,6);
}
}
class Fu {
int num;
Fu () {
System.out.println("Fu constructor... 0 " + "num = " + num);
num = 10;
}
Fu (int a) {
System.out.println("Fu constructor... 1 " + "num = " + num);
num = a;
}
}
class Zi extends Fu {
int num = 10;
Zi() {
System.out.println("Zi constructor... 0 " + "num = " + num + " fu num = " +super.num);
}
Zi(int a) {
this(a,0);
System.out.println("Zi constructor... 1 " + "num = " + num + " fu num = " +super.num);
}
Zi(int a, int b) {
super(a + b);
num = a + b;
System.out.println("Zi constructor... 2 " + "num = " + num + " fu num = " +super.num);
}
}
/*
结果:
Fu constructor... 0 num = 0
Zi constructor... 0 num = 10 fu num = 10
Fu constructor... 1 num = 0
Zi constructor... 2 num = 3 fu num = 3
Zi constructor... 1 num = 3 fu num = 3
Fu constructor... 1 num = 0
Zi constructor... 2 num = 11 fu num = 11
*/
子父类中,成员函数的特点
public class Sample {
public static void main(String[] args) {
Zi zi = new Zi();
zi.showA();
zi.showB();
zi.showC();
zi.showD();
}
}
class Fu {
void showA() {
System.out.println("Fu showA...");
}
void showC() {
System.out.println("Fu showC...");
}
private void showD() {
System.out.println("Fu showD...");
}
}
class Zi extends Fu{
void showB() {
System.out.println("Zi showB...");
}
@Override
void showC() {
System.out.println("Zi showC...");
}
void showD() {
System.out.println("Zi showD...");
}
}
/*
结果:
Fu showA...
Zi showB...
Zi showC...
Zi showD...
*/
- 如果父类有,子类没有,调用的是父类的
- 如果父类没有,子类有,调用的是子类的
- 如果父类有,子类也有,调用的是子类的(函数的重写/覆盖/Override)
- 如果父类有,但是为私有,则子类继承不到,除非子类自己写一个
重写的作用
严格意义上而言,子类并非是父类的一个子集。子类的内容很大程度上,很多情况下,都是父类的一种扩展或增量,重写仅仅去保留了父类的功能声明,但是具体的功能内容由子类来决定!
重写的规则
重写的时候,子类的权限必须大于等于父类的权限
重写的时候,返回值不能更改
public class Sample {
public static void main(String[] args) {
Zi zi = new Zi();
zi.showC(10);
zi.showC(10,20);
}
}
//public > 默认 > protected > private
class Fu {
public void show(){
System.out.println("Fu show...");
}
public int showB() {
return -1;
}
public void showC(int a){
System.out.println("Fu showC...");
}
}
class Zi extends Fu {
void show() {
System.out.println("Zi show...");
}
public void showB() {
}
public void showC(int a,int b){
System.out.println("Zi showC....");
}
}
/*
Sample.java:13: 错误: Zi中的show()无法覆盖Fu中的show()
void show() {
^
正在尝试分配更低的访问权限; 以前为public
1 个错误
*/
/*
Sample.java:21: 错误: Zi中的showB()无法覆盖Fu中的showB()
public void showB() {
^
返回类型void与int不兼容
*/
子父类中,静态成员的特点
静态变量的特点与成员变量是一致的
静态函数的特点与成员函数是一致的
public class Sample {
public static void main(String[] args) {
ArrayList list = new ArrayList();
for (int i = 1; i <= 12; i++) {
list.add(0,i);
}
System.out.println(list);
System.out.println(list.get(3));
System.out.println(list.delete(3));
System.out.println(list);
Stack stack = new Stack();
for (int i = 1; i <= 5; i++) {
stack.push(i);
}
System.out.println(stack);
System.out.println(stack.pop());
System.out.println(stack);
Queue queue = new Queue();
for (int i = 1; i <= 5; i++) {
queue.enqueue(i);
}
System.out.println(queue);
System.out.println(queue.dequeue());
System.out.println(queue);
}
}
class ArrayList {
private int[] data;
private int size;
int capacity = 10;
public ArrayList() {
data = new int[capacity];
size = 0;
}
//在指定角标处加入元素e
public void add(int index,int e) {
//1.对index做合法性处理
if (index < 0 || index > size) {
System.out.println(">>>插入位置不合法!");
return;
}
//2.考虑扩容的问题
if (size == data.length) {
resize(data.length * 2);
}
//3.合法加入 别忘了移动元素 不合法 结束并提示
for (int i = size - 1; i >= index; i--) {
data[i + 1] = data[i];
}
data[index] = e;
size++;
}
//删除指定角标处的元素 并返回
public int delete(int index) {
//1.角标的合法性
if (index < 0 || index >= size) {
System.out.println(">>>删除位置不合法!");
return -1;
}
//2.删除即可 别忘了移动元素
int ret = data[index];
for (int i = index + 1; i < size; i++) {
data[i - 1] = data[i];
}
//3.考虑缩容的问题
size--;
if (size == data.length / 4 && data.length > capacity) {
resize(data.length / 2);
}
return ret;
}
//获取指定角标处的元素
public int get(int index) {
//1.考虑角标的合法性
if (index < 0 || index >= size) {
System.out.println(">>>获取位置不合法!");
return -1;
}
//2.直接返回
return data[index];
}
//扩容+缩容的操作
private void resize(int newlength) {
int[] newData = new int[newlength];
for (int i = 0; i < size; i++) {
newData[i] = data[i];
}
data = newData;
}
public boolean isEmpty() {
return size == 0;
}
public int size() {
return size;
}
public String toString() {
//如果size == 0 返回 []
//如果不是[开始 拼接数字 每一个数字后面有个, 但是最后一个数字后面拼接]
String s = "[";
if (isEmpty()) {
s += "]";
} else {
for (int i = 0; i < size; i++) {
if (i == size - 1) {
s += data[i] + "]";
} else {
s += data[i] + ",";
}
}
}
return s;
}
}
class Stack extends ArrayList{
public Stack() {
super();
}
public void push(int e) {
add(size(),e);
}
public int pop() {
return delete(size() - 1);
}
public int peek() {
return get(size() - 1);
}
}
class Queue extends ArrayList {
public Queue() {
super();
}
public void enqueue(int e) {
add(size(),e);
}
public int dequeue() {
return delete(0);
}
public int front() {
return get(0);
}
public int rear() {
return get(size() - 1);
}
}
/*
结果:
[12,11,10,9,8,7,6,5,4,3,2,1]
9
9
[12,11,10,8,7,6,5,4,3,2,1]
[1,2,3,4,5]
5
[1,2,3,4]
[1,2,3,4,5]
1
[2,3,4,5]
*/
8.2 final 关键字
final:用于修饰不可改变内容,可以用于修饰类、函数和变量。
- 类:被修饰的类,不能被继承
- 函数:被修饰的方法,不能被重写
- 变量:被修饰的变量,不能被重新赋值
final修饰变量
表示该变量的值不可被改变
变量主要分为两种,基本数据类型、引用数据类型的变量
- final修饰的是基本数据类型变量 表示变量所存储的常量值不能改变
- final修饰的是引用数据类型变量 表示变量所存储的对象地址值不能改变 但是可以改变该对象中的数据(如果对象中的数据也是final 则也不能修改)
public class Sample {
public static void main(String[] args) {
final Demo d = new Demo();
//d = new Demo(); //error
//d.num = 30;//error
d.haha = 40;//ok
}
}
class Demo {
public final int num = 10;
public int haha = 20;
}
一般而言,当我们在定义常量(用变量名+final来表示),并定义成静态变量
public static final 数据类型 变量名 = 常量数据;
对于常量的变量名起名规则为 全部单词大写 单词与单词之间用下划线分隔
public static final int MAX_VALUE = 100;
final修饰函数
如果某一个类中的函数,不想让其子类去重写的话,该函数就可以声明为final类型
final修饰类
表示该类不能被继承
8.3 抽象类
定义
- 抽象方法:没有方法体的方法
- 抽象类:包含抽象方法的类
父类中的方法,被它的子类们重写,子类各自的实现都不尽相同。那么父类的方法声明和方法主体,只有声明还有意义,而方法主体则没有存在的意义了。我们把没有方法主体的方法称为抽象方法。Java语法规定,包含抽象方法的类就是抽象类。
抽象函数:当我们将多个事物的共同行为(函数)进行抽取并封装到另外一个类中时,发现在该类中,这些方法的具体执行内容无法确定,只能由这些子类来决定该函数的具体执行,那么在该类中,将这些抽取来的函数仅保留函数声明,但不保留函数体即可,那么该函数就是抽象函数,用abstract关键字来修饰,既然有了抽象函数的存在,那么具有抽象函数的类也被称之为抽象类,也必须用abstract修饰。抽象类不能创建对象,只有其实现子类能够创建对象
抽象方法
使用 abstract关键字修饰方法,该方法就成了抽象方法,抽象方法只包含一个方法名,而没有方法体。
public abstract class Animal {
public abstract void run();
}
抽象类的特点:
- 抽象类和抽象函数都需要被abstract修饰,抽象方法一定在抽象类中
- 抽象类不能创建对象,因为如果一旦创建对象,在调用其函数时,函数没有具体执行内容
- 只有覆盖了抽象类中所有的抽象函数后,子类才可以实例化。否则,该子类还是一个抽象类
抽象类的细节问题:
- 抽象类一定是一个父类,因为抽象类本身就是有多个事物进行抽取而来的
- 抽象类有成员变量、成员函数、构造函数,抽象类与一般类唯一的区别就是抽象类中抽象函数,其他一律相同(抽象类不能创建对象)
- 抽象类比一般类可以多定义一个成员:抽象函数
- 一般类可以创建对象,而抽象类不能创建对象
- 有抽象函数的类一定是抽象类,抽象类不一定有抽象函数!
- 抽象关键字abstract不能与以下关键字共存
- final:final修饰类,表示该类不能被继承;final修饰函数时,表示函数不能被重写;不能与absteact共存,抽象类本就是父类,并且其中的抽象函数就等着被子类重写。
- private:private修饰函数,表示函数被私有,不能被子类继承;不能与absteact共存,抽象函数就等着被子类重写。
- static:static修饰的函数,属于类的,随着类的加载从而被加载方法区中,和对象没有关系了,可以直接用类来调用静态成员,如果抽象函数被静态修饰,被类调用时没意义。
8.4接口
定义:它与定义类方式相似,但是使用 interface 关键字。它也会被编译成.class文件,但一定要明确它并不是类,而是另外一种引用数据类型。
当一个抽象类中,所有的方法都是抽象函数时,那么,该类就可以用接口来表示
接口不是类了,一些类的功能和操作不再适用于接口。
接口中没有成员函数,没有成员变量,没有构造函数,没静态函数,没有静态变量
接口也不能直接去创建对象
接口的使用,它不能创建对象,但是可以被实现( implements ,类似于被继承)。一个实现接口的类(可以看做是接口的子类),需要实现接口中所有的抽象方法,创建该类对象,就可以调用方法了,否则它必须是一个抽象类。
接口中的变量和函数就会有一些特殊的含义
- 接口中的变量 默认是公共静态常量 public static final类型 就算不写这些关键字 也是默认的
- 接口中的函数 默认是公共抽象的函数 public abstract 类型 就算不写这些关键字 也是默认的
接口中的特点
接口中的变量都是全局静态常量
接口中的方法都是全局抽象函数
接口不可以创建对象
子类必须覆盖接口中所有的抽象方法,或声明为abstract
类与接口之间可以存在多实现关系
接口与接口之间可以存在多继承关系
类与类之间只能是单继承关系
注:接口的存在, 主要解决的就是类与类之间只有单继承的关系
class DemoA {
void showA() {}
}
interface DemoB {
void showB();
}
//如果DemoC既有DemoA的描述 也有DemoB的描述
//也就是意味着DemoC可能要有两个父类 Java机制中不允许多父类的存在
class DemoC extends DemoA implements DemoB{
public void showA(){}
public void showB(){}
}
接口与抽象类的区别
相同点:
- 都位于继承或实现的顶端
- 都不能实例化
- 都包含抽象函数,其子类都必须覆盖这些方法
不同点: - 一个类只能继承一个父类,但是可以实现多个接口
- 抽象类中其实可以存在一些已经实现好的法,有部分未实现的方法由子类来决定;接口中只
能包含抽象函数,子类必须完全实现
8.5多态
8.5.1 多态的定义
定义:是指同一行为,具有多个不同表现形式
8.5.2 实现多态的前提
- 继承或者实现【二选一】
- 方法的重写【意义体现:不重写,无意义】
- 父类引用指向子类对象【格式体现】
8.5.3 多态的体现
父类类型 变量名 = new 子类对象;
变量名.方法名();
父类类型:指子类对象继承的父类类型,或者实现的父接口类型。
Fu f = new Zi();
f.method();
当使用多态方式调用方法时,首先检查父类中是否有该方法,如果没有,则编译错误;如果有,执行的是子类重写后方法。
代码如下:
public abstract class Animal {
public abstract void eat();
}
class Cat extends Animal {
public void eat() {
System.out.println("吃鱼");
}
}
class Dog extends Animal {
public void eat() {
System.out.println("吃骨头");
}
}
public class Test {
public static void main(String[] args) {
//多态形式,创建对象,向上转型
Animal cat = new Cat();
//先找父类中是否有eat()这个方法,没有编译报错,有的话调用子类中定义的方法体
cat.eat();
Animal dog = new Dog();
dog.eat();
}
}
8.5.4 多态的好处
实际开发的过程中,父类类型作为方法形式参数,传递子类对象给方法,进行方法的调用,更能体现出多态的扩展性与便利。
public abstract class Animal {
public abstract void eat();
}
class Cat extends Animal {
public void eat() {
System.out.println("吃鱼");
}
}
class Dog extends Animal {
public void eat() {
System.out.println("吃骨头");
}
}
public class Test {
public static void main(String[] args) {
// 多态形式,创建对象
Cat c = new Cat();
Dog d = new Dog();
// 调用showCatEat
showCatEat(c);
// 调用showDogEat
showDogEat(d);
/*
以上两个方法, 均可以被showAnimalEat(Animal a)方法所替代
而执行效果一致
*/
showAnimalEat(c);
showAnimalEat(d);
}
public static void showCatEat (Cat c){
c.eat();
}
public static void showDogEat (Dog d){
d.eat();
}
public static void showAnimalEat (Animal a){
a.eat();
}
}
优点:
面向父类/接口编程、关注的是父类/接口的能力、忽略了子类类型、可以使程序编写的更简单,并有良好的扩展。
缺点:
无法使用子类特有的属性和方法。
8.5.3 引用类型转换
向上转型:多态本身是子类类型向父类类型向上转换的过程,这个过程是默认的。
当父类引用指向一个子类对象时,便是向上转型,向上转型后,该类只可以使用父类中声明的方法,但调用的是子类中重写父类的方法,不可以调用子类中自己定义的方法。
父类类型 变量名 = new 子类类型();
如:Animal a = new Cat();
向下转型:父类类型向子类类型向下转换的过程,这个过程是强制的。
一个已经向上转型的子类对象,将父类引用转为子类引用,可以使用强制类型转换的格式,便是向下转型,向下转型后可以调用子类中定义的方法。
子类类型 变量名 = (子类类型) 父类变量名;
如:Cat c =(Cat) a;
为什么用转型
当使用多态方式调用方法时,首先检查父类中是否有该方法,如果没有,则编译错误。也就是说,不能调用子类拥有,而父类没有的方法。编译都错误,更别说运行了。这也是多态给我们带来的一点"小麻烦"。所以,想要调用子类特有的方法,必须做向下转型。
abstract class Animal {
abstract void eat();
}
class Cat extends Animal {
public void eat() {
System.out.println("吃鱼");
}
public void catchMouse() {
System.out.println("抓老鼠");
}
}
class Dog extends Animal {
public void eat() {
System.out.println("吃骨头");
}
public void watchHouse() {
System.out.println("看家");
}
}
public class Test {
public static void main(String[] args) {
// 向上转型,只能调用父类中定义的方法,不可以调用子类中定义的方法
Animal a = new Cat();
a.eat(); // 调用的是 Cat 的 eat
// 向下转型
Cat c = (Cat)a;
c.catchMouse(); // 调用的是 Cat 的 catchMouse
}
}
转型的异常
public class Test {
public static void main(String[] args) {
// 向上转型
Animal a = new Cat();
a.eat(); // 调用的是 Cat 的 eat
// 向下转型
Dog d = (Dog)a;
d.watchHouse(); // 调用的是 Dog 的 watchHouse 【运行报错】
}
}
这段代码可以通过编译,但是运行时,却报出了 ClassCastException ,类型转换异常!这是因为,明明创建了Cat类型对象,运行时,当然不能转换成Dog对象的。这两个类型并没有任何继承关系,不符合类型转换的定义。
为了避免ClassCastException 的发生,Java提供了== instanceof==关键字,给引用变量做类型的校验,格式如下:
变量名 instanceof 数据类型
如果变量属于该数据类型,返回true。
如果变量不属于该数据类型,返回false。
public class Test {
public static void main(String[] args) {
// 向上转型
Animal a = new Cat();
a.eat(); // 调用的是 Cat 的 eat
// 向下转型
if (a instanceof Cat){
Cat c = (Cat)a;
c.catchMouse(); // 调用的是 Cat 的 catchMouse
} else if (a instanceof Dog){
Dog d = (Dog)a;
d.watchHouse(); // 调用的是 Dog 的 watchHouse
}
}
}
8.6 内部类
8.6.1 概述
定义:将一个类A定义在另一个类B里面,里面的那个类A就称为内部类,B则称为外部类。
8.6.2 成员内部类
成员内部类 :定义在类中方法外的类。
class 外部类 {
class 内部类{
}
}
在描述事物时,若一个事物内部还包含其他事物,就可以使用内部类这种结构。比如,汽车类 Car 中包含发动机类Engine,这时, Engine 就可以使用内部类来描述,定义在成员位置。
class Car { //外部类
class Engine { //内部类
}
}
8.6.3 访问特点
- 内部类可以直接访问外部类的成员,包括私有成员
- 外部类要访问内部类的成员,必须要建立内部类的对象
创建内部类对象格式:
外部类名.内部类名 对象名 = new 外部类型().new 内部类型();
外部类名.内部类名 对象名 = new 外部类型.内部类型();
Outter.InnerB b=new Outter().new InnerB();
Outter.InnerB b=new Outter.InnerB();
public class Person {
private boolean live = true;
class Heart {
public void jump() {
// 直接访问外部类成员
if (live) {
System.out.println("心脏在跳动");
} else {
System.out.println("心脏不跳了");
}
}
}
public boolean isLive() {
return live;
}
public void setLive(boolean live) {
this.live = live;
}
}
public class InnerDemo {
public static void main(String[] args) {
// 创建外部类对象
Person p = new Person();
// 创建内部类对象
Heart heart = p.new Heart();
// 调用内部类方法
heart.jump();
// 调用外部类方法
p.setLive(false);
// 调用内部类方法
heart.jump();
}
}
/*
输出结果:
心脏在跳动
心脏不跳了
*/
内部类仍然是一个独立的类,在编译之后会内部类会被编译成独立的.class文件,但是前面冠以外部类的类名和$ 符号 。
比如,Person $ Heart.class
8.6.4 匿名内部类
匿名内部类 :是内部类的简化写法。它的本质是一个 带具体实现的 父类或者父接口的 匿名的 子类对象。开发中,最常用到的内部类就是匿名内部类了。以接口举例,当你使用一个接口时,似乎得做如下几步操作:
- 定义子类
- 重写接口中的方法
- 创建子类对象
- 调用重写后的方法
而匿名内部类可以简化上面的方法,为我们调用方法提供快捷的方式。
前提
匿名内部类必须继承一个父类或者实现一个父接口。
new 父类名或者接口名(){
// 方法重写
@Override
public void method() {
// 执行语句
}
};
举例
public abstract class FlyAble{
public abstract void fly();
}
public class InnerDemo {
public static void main(String[] args) {
/*
1.等号右边:是匿名内部类,定义并创建该接口的子类对象
2.等号左边:是多态赋值,接口类型引用指向子类对象
*/
FlyAble f = new FlyAble(){
public void fly() {
System.out.println("我飞了~~~");
}
};
//调用 fly方法,执行重写后的方法
f.fly();
}
}
通常在方法的形式参数是接口或者抽象类时,也可以将匿名内部类作为参数传递。代码如下:
public class InnerDemo2 {
public static void main(String[] args) {
/*
1.等号右边:定义并创建该接口的子类对象
2.等号左边:是多态,接口类型引用指向子类对象
*/
FlyAble f = new FlyAble(){
public void fly() {
System.out.println("我飞了~~~");
}
};
// 将f传递给showFly方法中
showFly(f);
}
public static void showFly(FlyAble f) {
f.fly();
}
}
以上两步,也可以简化为一步,代码如下:
public class InnerDemo3 {
public static void main(String[] args) {
/*
创建匿名内部类,直接传递给showFly(FlyAble f)
*/
showFly( new FlyAble(){
public void fly() {
System.out.println("我飞了~~~");
}
});
}
public static void showFly(FlyAble f) {
f.fly();
}
}
8.7 包与权限
8.7.1 包
定义:包其实相当于操作系统的文件夹
好处:
- 管理java文件的:方便寻找(包名+类名)、解决重名的问题
- 保护资源 (结合着访问控制符,来限定资源的访问)
定义包名:
- 一般都用小写英文
- 见名之义 公司域名的倒写+ 【部门名称】+项目名称+模块 、用.分隔,不能以.开头
包的声明:
package com.openlab.student.entity;
必须位于类的第一行非注释语句
包的导入:
- import java.util.Scanner;:类的完全限定名
- import java.util.*;:导入util包下的所有java类,但是不会导入子包中的类
导入的类重名,来自不同的包,需要显式的写出
8.7.2 权限
8.7.2.1 概述
在Java中提供了四种访问权限,使用不同的访问权限修饰符修饰时,被修饰的内容会有不同的访问权限。
- public:公共的
- protected:受保护的
- default:默认的
- private:私有的
public | protected | default | private | |
---|---|---|---|---|
同一类中 | √ | √ | √ | √ |
同一包中(子类与无关类) | √ | √ | √ | |
不同包的子类 | √ | √ | ||
不同包中的无关类 | √ |
public>protected>default>private
编写代码时,如果没有特殊的考虑,建议这样使用权限:
- 成员变量使用 private ,隐藏细节
- 构造方法使用 public ,方便创建对象
- 成员方法使用 public ,方便调用方法
不加权限修饰符,其访问能力与default修饰符相同
8.8 枚举类型
8.8.1 概述
枚举可以当成数据类型来用,管理一系列的常量(限定取值)
由来:
枚举出来之前都是拿class或者interface来组织管理基本数据类型的常量
缺点:只要数据类型合适,就会编译通过,不考虑实际的业务,可能造成错误
枚举:本质是Enum的子类,不能再继承别的类,可以限定取值
枚举的定义:
**public enum Role2 {
ROLE_NOMAL,
ROLE_VIP,
ROLE_SUPER_VIP
}
**
枚举可以当成数据类型来使用
private Role2 role;
枚举值的比较 equals或者==都可以
Role2 r=user.getRole();
if(r==Role2.ROLE_SUPER_VIP) {
System.out.println("超级用户。。。。。。。。。。。。");
}else if(r==Role2.ROLE_NOMAL) {
System.out.println("普通用户。。。。。。。。。。。。");
}
switch(r) {//byte int short String(1.7+),enum
case ROLE_SUPER_VIP:
System.out.println("超级用户。。。。。。。。。。。。");
break;
case ROLE_NOMAL:
System.out.println("普通用户。。。。。。。。。。。。");
break;
case ROLE_VIP:
System.out.println("VIP用户。。。。。。。。。。。。");
break;
}
第九章 常用类解析
9.1 Object类
9.1.1 概述
java.lang.Object
类是Java语言中的根类,即所有类的父类。它中描述的所有方法子类都可以使用。在对象实例化的时候,最终找的父类就是Object,如果一个类没有特别指定父类, 那么它默认继承自Object类。
主要方法
public boolean equals(Object obj)
:指示其他某个对象是否与此对象相等public String toString()
:返回该对象的字符串表示public final native Class<?> getClass()
:返回的是对象所属类的字节码对象(类的完全限定名)public native int hashCode()
:返回对象的哈希值,默认返回对象的地址,允许子类重写protected void finalize()
:当垃圾回收器确定不存在对该对象的更多引用时,由对象的垃圾回收器调用此方法
9.1.2 equals()方法
public boolean equals(Object obj)
:指示其他某个对象是否与此对象相等,默认使用==
判断两个对象是否相等,其实是比较对象的地址是否相等。
注意:参数是Object 需要向下转型,之前需要判断数据类型是否属于当前类型,允许子类自定义比较规则,通过子类重写equals()方法,自定义比较的规则。
实例
import java.util.Objects;
public class Person {
private String name;
private int age;
@Override
public boolean equals(Object obj) {
// 如果对象地址一样,则认为相同
if (this == obj)
return true;
// 如果参数为空,或者类型信息不一样,则认为不同
if (obj == null || !(obj instanceof Person))
return false;
// 向下转型
Person person = (Person) obj;
// 要求基本类型相等,并且将引用类型交给java.util.Objects类的equals静态方法取用结果
return age == person.age && Objects.equals(name, person.name);
}
}
String类中的equals()方法
String类重写类父类Object类中的equals()方法,它比较的是两个对象的值是否相等。
源码如下:
public boolean equals(Object anObject) {
//首先判断两个对象的地址是否相等
if (this == anObject) {
return true;
}
//在判断两个传入类是否为String 类,并向下转型后比较值是否相等
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
9.1.3 toString()方法
public String toString()
:把当前对象转换成字符串形式,未重写时,返回:类的完全限定名+@+哈希值的十六进制字符串
,可以继承,可以重写。
源码如下:
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
由于toString方法返回的结果是内存地址,而在开发中,经常需要按照对象的属性得到相应的字符串表现形式,因此也需要重写它。
public class Person {
private String name;
private int age;
@Override
public String toString() {
return "Person{" + "name='" + name + '\'' + ", age=" + age + '}';
}
}
9.1.4 getClass()方法
public final native Class<?> getClass()
:返回的是对象所属类的字节码对象(类的完全限定名)。
native: 本地的,修饰的方法,没有方法体,但是不代表没有实现,通常是由C/C++来实现的。
Class类 :字节码文件的类型,也是对象,类只加载一次。
9.2 StringBuffer和StringBuilder类
9.2.1 字符串拼接问题
由于String类的对象内容不可改变,所以每当进行字符串拼接时,总是会在内存中创建一个新的对象。例如:
public class StringDemo {
public static void main(String[] args) {
String s = "Hello";
s += "World";
System.out.println(s);
}
}
根据这句话分析我们的代码,其实总共产生了三个字符串,即"Hello"
、"World"
和"HelloWorld"
。引用变量s首先指向Hello
对象,最终指向拼接出来的新字符串对象,即HelloWord
。
由此可知,如果对字符串进行拼接操作,每次拼接,都会构建一个新的String对象,既耗时,又浪费空间。为了解决这一问题,可以使用java.lang.StringBuilder
和java.lang.StringBuffer
类。
9.2.3 StringBuffer和StringBuilder类的不同处
StringBuffer类
线程安全的可变字符序列,synchronized 同步锁、线程安全—适合多线程
StringBuilder类
不保证同步 ,非线程安全—适合单线程
9.2.4 常用的构造方法和方法
常用构造方法
-
public StringBuilder()
:构造一个空的StringBuilder容器。 -
public StringBuilder(String str)
:构造一个StringBuilder容器,并将字符串添加进去。
常用方法 -
public StringBuilder append(...)
:添加任意类型数据的字符串形式,并返回当前对象自身。 -
public String toString()
:将当前StringBuilder对象转换为String对象。 -
public StringBuffer insert(int offset, String str)
:指定位置插入字符串。 -
public StringBuffer reverse()
:翻转该字符串。 -
public String substring(int start, int end)
:截取指定开始 索引和结束索引位置的字符串。
9.3 基本数据类型与包装类
9.3.1 概述
Java提供了两个类型系统,基本数据类型与引用数据类型,使用基本数据类型在于效率,然而很多情况,会创建对象使用,因为对象可以做更多的功能,如果想要我们的基本类型像对象一样操作,就可以使用基本数据类型对应的包装类,如下:
基本数据类型 | 对应的包装类 |
---|---|
byte | Byte |
short | Short |
int | Integer |
long | Long |
float | Float |
double | Double |
char | Character |
boolean | Boolean |
9.3.2 包装类的继承关系
- Object(顶级父类)
- abstract class Number(父类)
- Byte、Short、Integer、Long、Float、Double(子类)
- Character(子类)
- Boolean(子类)
- abstract class Number(父类)
9.3.3 Integer为例
Integer类的定义
- public
final
class Integer extends Number implementsComparable<Integer>
final
:不能被继承- extends Number: 继承Number抽象类,实现很多方法,进而具有很多功能
Comparable<Integer>
:实现接口,可以比较
+public int compareTo(T o)
:重写比较方法,可以自定义(负数:小于、0:等于、正数:大于)
Integer类中的属性
MIN_VALUE
:-2147483648 =(-2^31)MAX_VALUE
: 2147483647 =(-2^31 - 1)TYPE
:获取int类对象SIZE
= 32: int数据占32位BYTES = SIZE / Byte.SIZE
: int数据占多少字节private final int value
:存储的就是Integer类的数据,本质上就是int value,value一旦赋值,不能更改- 直接用常量,可以节约资源
Integer类中的构造方法
Integer(int value)
:Integer(String s)
:要求s必须能转换成int,否则NumberFormatException, 如s=“a123”,报错,源码如下:
public Integer(String s) throws NumberFormatException {
//parseInt方法在下面
this.value = parseInt(s, 10);
}
Integer类中的常用方法
static int parseInt(String s)
static int parseInt(String s, int radix)
: 就是把字符串转换成int ,radix进制可选,源码如下:
public static int parseInt(String s, int radix) throws NumberFormatException {
if (s == null) {
throw new NumberFormatException("null");
}
//Character.MIN_RADIX=2
if (radix < Character.MIN_RADIX) {
throw new NumberFormatException("radix " + radix +
" less than Character.MIN_RADIX");
}
//Character.MAX_RADIX=32
if (radix > Character.MAX_RADIX) {
throw new NumberFormatException("radix " + radix + " greater than Character.MAX_RADIX");
}
int result = 0;
boolean negative = false;
int i = 0, len = s.length();
int limit = -Integer.MAX_VALUE;
int multmin;
int digit;
if (len > 0) {
char firstChar = s.charAt(0);
if (firstChar < '0') { // Possible leading "+" or "-"
if (firstChar == '-') {
negative = true;
limit = Integer.MIN_VALUE;
} else if (firstChar != '+')
throw NumberFormatException.forInputString(s);
if (len == 1) // Cannot have lone "+" or "-"
throw NumberFormatException.forInputString(s);
i++;
}
multmin = limit / radix;
while (i < len) {
// Accumulating negatively avoids surprises near MAX_VALUE
digit = Character.digit(s.charAt(i++),radix);
if (digit < 0) {
throw NumberFormatException.forInputString(s);
}
if (result < multmin) {
throw NumberFormatException.forInputString(s);
}
result *= radix;
if (result < limit + digit) {
throw NumberFormatException.forInputString(s);
}
result -= digit;
}
} else {
throw NumberFormatException.forInputString(s);
}
return negative ? result : -result;
}
boolean equals(Object obj)
:当类型相同且值相等,才返回true
public boolean equals(Object obj) {
if (obj instanceof Integer) {
return value == ((Integer)obj).intValue();
}
return false;
}
static Integer valueOf(int i)
static Integer valueOf(String s)
:把其它数据类型转换成当前类型,源码如下:
public static Integer valueOf(int i) {
//high默认为127,也可以自定义设置值
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
1、如果传入的整数是-128-high之间的数,直接从缓存中返回对象(此时地址 相同),否则就新创建一个Integer对象返回(新建一个地址)。
2、Integer类中有一个静态内部类IntegerCache
,内部类中有一个cache
,当Integer加载的时候,就会初始化此类,缓-128-high(一般就是127)
之间的数。
3、 如果你需要Integer类型的对象,优先使用valueOf(先从缓存中读取,节省内存),而不是构造方法。
4、Float、Double类的valueOf()
没有缓存,因为[-128,127] 整数数量是固定的,而小数却是无穷的。
小结
- Byte、Short、Integer、Long的valueOf()方法都有缓存,【-128,127】,超出范围创建对象
- Float、Double的valueOf()方法没有缓存,每次都会新创建对象
- Character 的valueOf()方法有缓存,【0,127】
- Boolean 的valueOf()方法有缓存,有静态的常量true,false,直接返回
9.3.4 Integer类的进制转换
- static String toHexString(int i) :16进制
- static String toOctalString(int i) :8进制
- static String toBinaryString(int i):2进制
9.3.5 常用的数据类型转换
int–>String
- a+""
- String.valueOf()
- Interger.toString(int i)
- String–int
- Integer.parse(String s)
- Integer-String
- Integer.toString()
- String–Integer
- Integer valueOf(String s, int radix)
- Integer(String s)
- int–Integer
- Integer(int i)
- Integer valueOf(int i)
- Integer–int
- int intValue()
9.3.6 装箱与拆箱
基本类型与对应的包装类对象之间,来回转换的过程称为装箱与拆箱:
- 装箱:从基本类型转换为对应的包装类对象。
- 本质:
Integer.valueOf()
- 本质:
Integer i=12;//自动装箱
Integer i2=Integer.valueOf(12);
//以上代码效果相同
//-------------------------
Integer num1=12;
Integer num2=12;
System.out.println(num1==num2);//true
/*本质:Integer.valueOf(int i),先判断该数是否在缓存中,在就返回缓存中的地址,否则就创建新的对象(地址也创建)
说明num1和num2地址相同,因为num1和num2都是从缓存中取的同一个地址
*/
//-------------------------
Integer num1=129;
Integer num2=129;
System.out.println(num1==num2);//false
//129没有缓存,每次都需要重写创建,所以num1和num2地址不同
//-------------------------
Double num1=12d;
Double num2=12d;
System.out.println(num1==num2);//false
//因为Double.valueOf(double i) 默认没有缓存
- 拆箱:从包装类对象转换为对应的基本类型。
- 本质:
Integer.intValue()
- 本质:
Integer i=12;//自动装箱
int num=i;//自动拆箱
不要频繁地装拆箱,浪费内存空间
9.3.7 什么时候装箱、拆箱?
Integer num1=400;
int num2=400;
System.out.println(num1==num2);//true
//返回true就可以说明num1发生了拆箱操作。换一句话来说,==链接包装类和基本数据类型的时候,就拆箱
Integer num1=400;
int num2=400;
System.out.println(num1.equals(num2));//true
//返回true就可以说明num2进行了装箱
Long num1=400l;
int num2=400;
System.out.println(num1.equals(num2));//false
//原因:long类型的equals方法要求类型相同且值相同才返回true,而num2自身装箱成了Integer类型
Integer num1=400;
int num2=400;
System.out.println(num1*num2);
//当+、-、*、/、%链接包装类和基本数据类型的时候,是拆箱
//拆箱的时候注意避免空指针异常
Integer num1=null;
int num2=num1;//java.lang.NullPointerException
//拆箱实际在执行intValue()方法
小结
- 在包装类和基本数据类型中用
==、+、-、*、/、%
时会将包装类转换为基本数据类型(拆箱)来进行比较和运算。 - 在包装类和基本数据类型中调用包装类的方法时,会将基本数据类型转换为包装类(装箱)来判断比较。
9.3.8 Character类的常用方法
System.out.println(Character.isDigit('a'));
System.out.println(Character.isLetter('a'));
System.out.println(Character.isWhitespace(' '));
System.out.println(Character.isUpperCase('A'));
System.out.println(Character.isLowerCase('b'));
System.out.println(Character.toLowerCase('U'));
9.3.9 BigInteger和BigDecimal
- BigInteger
- 做权限控制
BigInteger setBit(int n)
: bi+2^nboolean testBit(int n)
:判断n是否在bi+2^n中
public class TestBigInt {
public static void main(String[] args) {
//后台管理员设置
BigInteger bi=new BigInteger("0");
bi=bi.setBit(2);//4 0+2^2
bi=bi.setBit(3);//12 4+2^3
System.out.println(bi);//setBit(n) bi+2^n
//服务器端判断校验
BigInteger bi2=new BigInteger("36");
System.out.println(bi2.testBit(5));
}
}
第十章 异常
10.1 异常概述
异常 :指的是程序在执行过程中,出现的非正常的情况,最终会导致JVM的非正常停止。
在Java等面向对象的编程语言中,异常本身是一个类,产生异常就是创建异常对象并抛出了一个异常对象,Java处理异常的方式是中断处理。
异常指的并不是语法错误,语法错了,编译不通过,不会产生字节码文件,根本不能运行.
描述一个异常:
Exception in thread "main" java.lang.ArithmeticException: / by zero
at Test.main(Test.java:5)
- 异常名称:java.lang.
ArithmeticException
- 异常出现的原因:/ by zero
- 异常出现的位置:at Test.main(Test.java:5)
10.2 异常体系
异常机制其实是帮助我们找到程序中的问题,异常的根类是java.lang.Throwable
,其下有两个子类:java.lang.Error
与java.lang.Exception
,平常所说的异常指 java.lang.Exception
。
Throwable体系:
Error
:严重错误Error,无法通过处理的错误,只能事先避免。OutOfMemoryError
:堆内存溢出(数组定义长度过长)StackOverflowError
: 栈溢出(递归循环调用)
Exception
:表示异常,异常产生后程序员可以通过代码的方式纠正,使程序继续运行,是必须要处理的。RuntimeException
:运行时异常,不强制要求程序处理ArrayIndexOutOfBoundsException
: 数组下标越界异常NullPointerException
: 空指针异常,对象调用空的属性或方法InputMismatchException
: 输入不匹配异常ArithmeticException
: 算术异常ClassCastException
: 类型转换异常NegativeArrayException
:数组负下标异常StringIndexOutOfBoundsException
:字符串索引越界异常IOException
:输入输出异常NumberFormatException
:数字格式化异常
Throwable中的常用方法:
public void printStackTrace()
:打印异常的详细信息,包含了异常的类型,异常的原因,还包括异常出现的位置,在开发和调试阶段,都得使用printStackTrace。public String getMessage()
:获取发生异常的原因,提示给用户的时候,就提示错误原因。public String toString()
:获取异常的类型和异常描述信息(不用)。
Throwable中的常用方法:
public void printStackTrace()
:打印异常的详细信息,包含了异常的类型,异常的原因,还包括异常出现的位置,在开发和调试阶段,都得使用printStackTrace。public String getMessage()
:获取发生异常的原因,提示给用户的时候,就提示错误原因。public String toString()
:获取异常的类型和异常描述信息(不用)。
10.3 异常分类
异常(Exception)的分类:
- 编译时期异常:checked异常。在编译时期,就会检查,如果没有处理异常,则编译失败,外界的原因,强制要求程序处理。(如日期格式化异常)
IOException
ClassNotFoundException
ParseException
- 运行时期异常:runtime异常。在运行时期,检查异常,在编译时期,运行异常不会被编译器检测(不报错),不强制要求程序处理。(如数学异常)
ArrayIndexOutOfBoundsException NullPointerException
InputMismatchException ArithmeticException ClassCastException
10.4 异常的处理
- 方式一:添加判断 if…else
缺点:程序员会把精力放在避免异常,无法集中在业务上了。一直在补漏洞,但是也不一定能补全业务代码,程序被处理异常的代码淹没了,可读性不强。 - 方式二:异常处理机制,预置一些异常处理程序,如果有异常情况,执行相应的处理程序,这样就不会让程序中断。
10.4.1 异常处理机制
Java异常处理的五个关键字:try、catch、finally、throw、throws
。
10.4.1.1 捕获异常try…catch
其中try必选,catch和finaly可选,但是必有其一
try{
//可能出现异常的代码
}catch(){
//异常出现的话,如何处理---异常处理程序
//记录日志/打印异常信息/继续抛出异常
}catch(){
//异常出现的话,如何处理---异常处理程序
//记录日志/打印异常信息/继续抛出异常
}
finally{
//不管是否出现异常,都会去执行的代码,关闭资源
}
执行流程
- 正常情况下
执行try,在执行finally
中的代码,然后在执行后续代码 - 异常情况下
出现异常之后的代码不再执行,转到catch块执行,在执行finally
中的代码,然后在执行后续代码
catch块的注意点
- 异常除以可以有多个catch来捕获多个异常,catch是从上往下匹配,上面的匹配上,下面的就不再匹配,所以,catch捕获的异常类型应该从小到大来写,否则会报错,因为有些catch块永远也执行不了。
- 如果某些异常的处理程序是一样的,可以用|来连接多个异常类型
catch(InputMismatchException|ArithmeticException ex )
10.4.1.2 finally 代码块
finally:有一些特定的代码无论异常是否发生,都需要执行。另外,因为异常会引发程序跳转,导致有些语句执行不到。而finally就是解决这个问题的,在finally代码块中存放的代码都是一定会被执行的。
finally中return的执行流程
- 当
try
和catch
中都有return语句,先执行return前的语句,再执行finally,最后return。 - 当
try、catch、finally
中都有return
语句,先执行逻辑代码,当执行到finally是直接return。
public class Test4 {
public static void main(String[] args) {
//除法
System.out.println(divide());
}
private static int divide() {
// int status=0;//0正常 -1 InputMismatchException -2ArithmeticException
Scanner input=new Scanner(System.in);
try {
System.out.println("请输入一个数");
int num1=input.nextInt();//InputMismatchException Exception
System.out.println("再输入一个数");
int num2=input.nextInt();//InputMismatchException
int result=num1/num2;//ArithmeticException
System.out.println("结果是"+result);
return 0;
}catch(InputMismatchException ex) {
System.out.println("只能输入数字");
return -1;
}catch(ArithmeticException ex) {
System.out.println("除数不能为0 "+ex.getMessage());
return -2;
}catch (Exception e) {
System.out.println("其它异常");
return -3;
}finally {
System.out.println("程序结束");
}
}
}
/*
结果1(无异常):
请输入一个数
12
再输入一个数
3
结果是4
程序结束
0
结果2(有异常):
请输入一个数
a
只能输入数字
程序结束
-1
*/
try {
System.out.println("请输入一个数");
int num1=input.nextInt();//InputMismatchException Exception
System.out.println("再输入一个数");
int num2=input.nextInt();//InputMismatchException
int result=num1/num2;//ArithmeticException
System.out.println("结果是"+result);
return 0;
}catch(InputMismatchException ex) {
System.out.println("只能输入数字");
return -1;
}catch(ArithmeticException ex) {
System.out.println("除数不能为0 "+ex.getMessage());
return -2;
}catch (Exception e) {
System.out.println("其它异常");
return -3;
}finally {
System.out.println("程序结束");
return -4;
}
/*
结果:
请输入一个数
3
再输入一个数
33
结果是0
程序结束
-4
*/
10.4.1.3 抛出异常throw
java中,提供了一个throw关键字,它用来抛出一个指定的异常对象,throw用在方法内,用来抛出一个异常对象,将这个异常对象传递到调用者处,并结束当前方法的执行。
使用格式
throw new 异常类名(参数);
throw new NullPointerException("要访问的arr数组不存在");
throw new ArrayIndexOutOfBoundsException("该索引在数组中不存在,已超出范围");
注意:
- 如果产生了问题,我们就会throw将问题描述类即异常进行抛出,也就是将问题返回给该方法的调用者。
- 对于调用者来说,要么是进行捕获处理,要么就是继续讲问题声明出去,使用throws声明
10.4.1.3 声明异常throws
声明异常:将问题标识出来,报告给调用者。如果方法内通过throw抛出了编译时异常,而没有捕获处理,那么必须通过throws进行声明,让调用者去处理。
关键字throws运用于方法声明之上,用于表示当前方法不处理异常,而是提醒该方法的调用者来处理异常(抛出异常)。
声明异常格式:
修饰符 返回值类型 方法名(参数) throws 异常类名1,异常类名2…{ }
throws用于进行异常类的声明,若该方法可能有多种异常情况产生,那么在throws后面可以写多个异常类,用逗号隔开。
- 运行时异常,不强制要求捕获或者声明。即使声明了,也不要求调用者必须捕获或者声明。所以,我们一般不声明,但是往往会捕获。
- 编译时异常,强制要求捕获或声明。如果方法内部不捕获,那么调用者或者上层调用者就必须捕获。否泽,编译会出错。
10.5 异常注意事项
多个异常使用捕获又该如何处理呢?
- 多个异常分别处理。
- 多个异常一次捕获,多次处理。
- 多个异常一次捕获一次处理。
一般我们是使用一次捕获多次处理方式,格式如下:
try{
编写可能会出现异常的代码
}catch(异常类型A e){ 当try中出现A类型异常,就用该catch来捕获.
处理异常的代码
//记录日志/打印异常信息/继续抛出异常
}catch(异常类型B e){ 当try中出现B类型异常,就用该catch来捕获.
处理异常的代码
//记录日志/打印异常信息/继续抛出异常
}
注意:
这种异常处理方式,要求多个catch中的异常不能相同,并且若catch中的多个异常之间有子父类异常的关系,那么子类异常要求在上面的catch处理,父类异常在下面的catch处理。
-1. 运行时异常被抛出可以不处理。即不捕获也不声明抛出。
2. 如果finally有return语句,永远返回finally中的结果,避免该情况.
3. 如果父类抛出了多个异常,子类重写父类方法时,抛出和父类相同的异常或者是父类异常的子类或者不抛出异常。
4. 父类方法没有抛出异常,子类重写父类该方法时也不可抛出异常。此时子类产生该异常,只能捕获处理,不能声明抛出。
10.6 自定义异常
异常类如何定义:
- 自定义一个编译期异常: 自定义类 并继承于
java.lang.Exception
。 - 自定义一个运行时期的异常类:自定义类 并继承于
java.lang.RuntimeException
。
注意
- 可以重写带参构造,设置异常原因。
- 自定义异常只能手动抛出,不能让JVM抛。
// 业务逻辑异常
public class RegisterException extends Exception {
/**
* 空参构造
*/
public RegisterException() {
}
/**
*
* @param message 表示异常提示
*/
public RegisterException(String message) {
super(message);
}
/**
*
* @param cause 表示传入的异常类,可以将异常类进行转换
*/
public RegisterException(Throwable cause) {
super(cause);
}
}
第十一章 集合
11.1 Collection集合
11.1.1 集合概述
集合:集合是java中提供的一种容器,可以用来存储多个数据。
集合和数组的区别:
- 数组的长度是固定的,集合的长度是可变的。
- 数组中存储的是同一类型的元素,可以存储基本数据类型值,存储的是单列数据。集合存储的都是对象。而且对象的类型可以不一致。在开发中一般当对象多的时候,使用集合进行存储。
11.1.2 集合框架
集合按照其存储结构可以分为两大类,分别是单列集合java.util.Collection
和双列集合java.util.Map
。
- Collection:单列集合类的根接口,用于存储一系列符合某种规则的元素,它有两个重要的子接口,分别是
java.util.List
和java.util.Set
。其中,List的特点是元素有序、元素可重复,有索引,允许为null。Set的特点是元素无序,而且不可重复,最多包含一个null。List接口的主要实现类有java.util.ArrayList
和java.util.LinkedList
,Set接口的主要实现类有java.util.HashSet
和java.util.TreeSet
。
线程安全集合如下:
- Vector
- HashTable
- Properties
线程不安全集合如下:
- ArrayList
- LinkedList
- HashSet
- TreeSet
- HashMap
- TreeMap
11.1.3 Collection常用方法
Collection是所有单列集合的父接口,因此在Collection中定义了单列集合(List和Set)通用的一些方法,这些方法可用于操作所有的单列集合。方法如下:
public boolean add(E e)
:把给定的对象添加到当前集合中public void clear()
:清空集合中所有的元素public boolean remove(E e)
:把给定的对象在当前集合中删除public boolean contains(E e)
:判断当前集合中是否包含给定的对象public boolean isEmpty()
:判断当前集合是否为空public int size()
:返回集合中元素的个数public Object[] toArray()
:把集合中的元素,存储到数组中public Iterator<E> iterator()
:返回一个迭代器,遍历集合
11.2 Iterator迭代器
11.2.1 Iterator接口
在程序开发中,经常需要遍历集合中的所有元素。针对这种需求,JDK专门提供了一个接口java.util.Iterator
。Iterator
接口也是Java集合中的一员,但它与Collection、Map
接口有所不同,Collection接口与Map接口主要用于存储元素,而Iterator主要用于迭代访问(即遍历)Collection
中的元素,因此Iterator
对象也被称为迭代器。
public Iterator iterator()
:获取集合对应的迭代器,用来遍历集合中的元素
迭代的概念:
即Collection集合元素的通用获取方式。在取元素之前先要判断集合中有没有元素,如果有,就把这个元素取出来,继续在判断,如果还有就再取出出来。一直把集合中的所有元素全部取出。这种取出方式专业术语称为迭代。
Iterator接口的常用方法如下:
public E next()
:返回迭代的下一个元素public boolean hasNext()
:如果仍有元素可以迭代,则返回 true
基本使用方法:
public class IteratorDemo {
public static void main(String[] args) {
// 使用多态方式 创建对象
Collection<String> coll = new ArrayList<String>();
// 添加元素到集合
coll.add("串串星人");
coll.add("吐槽星人");
coll.add("汪星人");
//遍历
//使用迭代器 遍历 每个集合对象都有自己的迭代器
Iterator<String> it = coll.iterator();
// 泛型指的是 迭代出 元素的数据类型
while(it.hasNext()){ //判断是否有迭代元素
String s = it.next();//获取迭代出的元素
if("汪星人".equals(s )) {
// //ConcurrentModificationException
// coll.remove(s);//添加或移除时,modCount改变了与期望的modCount值不同,会抛出异常,所以下轮循环next()方法报错
it.remove();//安全的
}
System.out.println(s);
}
}
}
注意:在进行集合元素取出时,如果集合中已经没有元素了,还继续使用迭代器的next方法,将会发生
java.util.NoSuchElementException
没有集合元素的错误,在迭代是不可以使用集合对象操作集合中的元素,否则会有ConcurrentModificationException
异常,要使用迭代对象来移除。
11.2.3 增强for
增强for循环(也称for each循环)是JDK1.5以后出来的一个高级for循环,专门用来遍历数组和集合的。它的内部原理其实是个Iterator迭代器,所以在遍历的过程中,不能对集合中的元素进行增删操作。
格式:
for(元素的数据类型 变量 : Collection集合or数组){
//写操作代码
}
遍历数组和集合:
public class NBForDemo1 {
public static void main(String[] args) {
//遍历数组
int[] arr = {3,5,6,87};
//使用增强for遍历数组
for(int a : arr){//a代表数组中的每个元素
System.out.println(a);
}
//遍历集合
Collection<String> coll = new ArrayList<String>();
coll.add("小河神");
coll.add("老河神");
coll.add("神婆");
//使用增强for遍历
for(String s :coll){//接收变量s代表 代表被遍历到的集合元素
System.out.println(s);
}
}
}
11.3 泛型
11.3.1 泛型概述
public class GenericDemo {
public static void main(String[] args) {
Collection coll = new ArrayList();
coll.add("abc");
coll.add("itcast");
coll.add(5);//由于集合没有做任何限定,任何类型都可以给其中存放
Iterator it = coll.iterator();
while(it.hasNext()){
//需要打印每个字符串的长度,就要把迭代出来的对象转成String类型
String str = (String) it.next();
System.out.println(str.length());
}
}
}
程序在运行时发生了异常
java.lang.ClassCastException
,由于集合中什么类型的元素都可以存储,导致取出时强转引发运行时ClassCastException
,可以使用泛型来解决这类问题。
泛型:可以在类或方法中预支地使用未知的类型
一般在创建对象时,将未知的类型确定具体的类型,当没有指定泛型时,默认类型为Object类型。泛型是数据类型的一部分,我们将类名与泛型合并一起看做数据类型。
使用泛型的好处:
- 将运行时期的
ClassCastException
,转移到了编译时期变成了编译失败 - 避免了类型强转的麻烦
11.3.2 泛型的定义与使用
泛型,用来灵活地将数据类型应用到不同的类、方法、接口当中,将数据类型作为参数进行传递。
泛型常用字符:
E(element) 、K(key)、V(value)、 T(type)。
含有泛型的类
定义格式:
修饰符 class 类名<代表泛型的变量> { }
使用泛型: 在创建对象的时候确定泛型。
案例:
定义一个泛型类:
public class MyGenericClass<MVP> {
//没有MVP类型,在这里代表 未知的一种数据类型 未来传递什么就是什么类型
private MVP mvp;
public void setMVP(MVP mvp) {
this.mvp = mvp;
}
public MVP getMVP() {
return mvp;
}
}
使用泛型类:
public class GenericClassDemo {
public static void main(String[] args) {
// 创建一个泛型为String的类
MyGenericClass<String> my = new MyGenericClass<String>();
// 调用setMVP
my.setMVP("大胡子登登");
// 调用getMVP
String mvp = my.getMVP();
System.out.println(mvp);
//创建一个泛型为Integer的类
MyGenericClass<Integer> my2 = new MyGenericClass<Integer>();
my2.setMVP(123);
Integer mvp2 = my2.getMVP();
}
}
11.3.2.2 含有泛型的方法
定义格式:
修饰符 <代表泛型的变量> 返回值类型 方法名(参数){ }
案例:
定义泛型方法:
public class MyGenericMethod {
public <MVP> void show(MVP mvp) {
System.out.println(mvp.getClass());
}
public <MVP> MVP show2(MVP mvp) {
return mvp;
}
}
使用泛型方法:
public class GenericMethodDemo {
public static void main(String[] args) {
// 创建对象
MyGenericMethod mm = new MyGenericMethod();
// 演示看方法提示
mm.show("aaa");
mm.show(123);
mm.show(12.45);
}
}
11.3.2.3 含有泛型的接口
定义格式:
修饰符 interface接口名<代表泛型的变量> { }
案例:
定义一个泛型接口:
public interface MyGenericInterface<E>{
public abstract void add(E e);
public abstract E getE();
}
使用方式1,定义类时确定泛型的类型
public class MyImp1 implements MyGenericInterface<String> {
@Override
public void add(String e) {
// 省略...
}
@Override
public String getE() {
return null;
}
}
使用方式2,始终不确定泛型的类型,直到创建对象时,确定泛型的类型
public class MyImp2<E> implements MyGenericInterface<E> {
@Override
public void add(E e) {
// 省略...
}
@Override
public E getE() {
return null;
}
}
public class GenericInterface {
public static void main(String[] args) {
MyImp2<String> my = new MyImp2<String>();
my.add("aa");
}
}
11.3.3 泛型通配符
11.3.3.1 通配符基本使用
泛型的通配符:
不知道使用什么类型来接收的时候,此时可以使用
?
,?
表示未知通配符,此时只能接受数据,不能往该集合中存储数据。
public static void main(String[] args) {
Collection<Integer> list1 = new ArrayList<Integer>();
getElement(list1);
Collection<String> list2 = new ArrayList<String>();
getElement(list2);
}
public static void getElement(Collection<?> coll){}
//?代表可以接收任意类型
泛型不存在继承关系 Collection list = new ArrayList();这种是错误的。
11.3.3.2 受限泛型
之前设置泛型的时候,实际上是可以任意设置的,只要是类就可以设置。但是在JAVA的泛型中可以指定一个泛型的上限和下限。
泛型的上限:
- 格式:
类型名称 <? extends 类 > 对象名称
- 意义:
只能接收该类型及其子类
泛型的下限:
- 格式:
类型名称 <? super 类 > 对象名称
- 意义:
只能接收该类型及其父类型
比如:现已知Object类,String 类,Number类,Integer类,其中Number是Integer的父类
public static void main(String[] args) {
Collection<Integer> list1 = new ArrayList<Integer>();
Collection<String> list2 = new ArrayList<String>();
Collection<Number> list3 = new ArrayList<Number>();
Collection<Object> list4 = new ArrayList<Object>();
getElement1(list1);
getElement1(list2);//报错
getElement1(list3);
getElement1(list4);//报错
getElement2(list1);//报错
getElement2(list2);//报错
getElement2(list3);
getElement2(list4);
}
// 泛型的上限:此时的泛型?,必须是Number类型或者Number类型的子类
public static void getElement1(Collection<? extends Number> coll){}
// 泛型的下限:此时的泛型?,必须是Number类型或者Number类型的父类
public static void getElement2(Collection<? super Number> coll){}
11.4 常见的数据结构
数据存储的常用结构有:栈、队列、数组、链表和红黑树。
11.4.1 栈
栈:stack,又称堆栈,它是运算受限的线性表,其限制是仅允许在特定的一端进行插入和删除操作,不允许在其他任何位置进行添加、查找、删除等操作。
栈的特点:
- 先进后出,后进先出(即,存进去的元素,要在后它后面的元素依次取出后,才能取出该元素)
- 栈的入口、出口的都是栈的顶端位置
注意:
压栈:就是存元素。即,把元素存储到栈的顶端位置,栈中已有元素依次向栈底方向移动一个位置。
弹栈:就是取元素。即,把栈的顶端位置元素取出,栈中已有元素依次向栈顶方向移动一个位置。
11.4.2 队列
队列:queue,简称队,它同堆栈一样,也是一种运算受限的线性表,其限制是仅允许在表的一端进行插入,而在表的另一端进行删除。
队列特点:
- 先进先出,后进后出(即,存进去的元素,要在后它前面的元素依次取出后,才能取出该元素)
- 队列的入口、出口各占一侧
11.4.3 数组
数组:Array,是有序的元素序列,数组是在内存中开辟一段连续的空间,并在此空间存放元素,每个元素有一个索引。
数组的特点:
- 查找元素快:通过索引,可以快速访问指定位置的元素
- 增删元素慢:
1. 指定索引位置增加元素:需要创建一个新数组,将指定新元素存储在指定索引位置,再把原数组元素根据索引,复制到新数组对(移动插入位置后面的元素)
2. 指定索引位置删除元素:需要创建一个新数组,把原数组元素根据索引,复制到新数组对应索引的位置,原数组中指定索引位置(移动删除位置后面的元素)
11.4.4 链表
链表:linked list,由一系列结点node(链表中每一个元素称为结点)组成,结点可以在运行时i动态生成。每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。我们常说的链表结构有单向链表与双向链表,那么这里给大家介绍的是单向链表。
链表的特点:
- 多个结点之间,通过地址(指针)进行连接
- 查找元素慢:想查找某个元素,需要通过连接的节点,依次向后查找指定元素
- 增删元素快:
增加元素:只需要修改连接下个元素的地址即可
删除元素:只需要修改连接下个元素的地址即可
11.5 List接口
11.5.1 概述
java.util.List
接口继承自Collection
接口,是单列集合的一个重要分支,习惯性地会将实现了List接口的对象称为List集合。在List集合中允许出现重复的元素,所有的元素是以一种线性方式进行存储的,在程序中可以通过索引来访问集合中的指定元素。另外,List集合还有一个特点就是元素有序,即元素的存入顺序和取出顺序一致。
11.5.2 List集合的特点
- 它是一个元素存取有序的集合
- 它是一个带有索引的集合,通过索引就可以精确的操作集合中的元素
- 集合中可以有重复的元素,通过元素的equals方法,来比较是否为重复的元素
- 允许null元素
11.5.2 List接口中常用方法
List作为Collection集合的子接口,不但继承了Collection接口中的全部方法,而且还增加了一些根据元素索引来操作集合的特有方法,如下:
public void add(int index, E element)
:将指定的元素,添加到该集合中的指定位置上public E get(int index)
:返回集合中指定位置的元素。public E remove(int index)
:移除列表中指定位置的元素, 返回的是被移除的元素public E set(int index, E element)
:用指定元素替换集合中指定位置的元素,返回值的更新前的元素
11.6 List接口的子类
11.6.1 ArrayList集合
java.util.ArrayList
集合数据存储的结构是数组结构,是一个可变长的数组。元素增删慢,查找快,由于日常开发中使用最多的功能为查询数据、遍历数据,所以ArrayList
是最常用的集合。
特点:
- 访问、修改速度快
- 添加、删除速度慢
- 连续空间,浪费空间
- 此实现不是同步的,不是线程安全
11.6.2 LinkedList集合
java.util.LinkedList
集合数据存储的结构是链表结构。方便元素添加、删除的集合,LinkedList
是一个双向链表。
特点:
- 随机访问效率低
- 增删快
- 此实现不是同步的,不是线程安全
LinkedList常用方法:
public void addFirst(E e)
:将指定元素插入此列表的开头。public void addLast(E e)
:将指定元素添加到此列表的结尾。public E getFirst()
:返回此列表的第一个元素。public E getLast()
:返回此列表的最后一个元素。public E removeFirst()
:移除并返回此列表的第一个元素。public E removeLast()
:移除并返回此列表的最后一个元素。public E pop()
:从此列表所表示的堆栈处弹出一个元素。public void push(E e)
:将元素推入此列表所表示的堆栈。public boolean isEmpty()
:如果列表不包含元素,则返回true。
LinkedList集合也可以作为堆栈,队列的结构使用
11.7 Set接口
11.7.1 概述
java.util.Set
接口和java.util.List
接口一样,同样继承自Collection接口,它与Collection接口中的方法基本一致,并没有对Collection接口进行功能上的扩充,只是比Collection接口更加严格了。与List接口不同的是,Set接口中元素无序,并且都会以某种规则保证存入的元素不出现重复。
Set集合取出元素的方式可以采用:迭代器、增强for
11.7.2 Set集合特点
- 无序,不重复
- 最多包含一个 null 元素
11.8 Set接口的子类
11.8.1 HashSet集合
java.util.HashSet
是Set接口的一个实现类,它所存储的元素是不可重复的,并且元素都是无序的(即存取顺序不一致)。java.util.HashSet
底层的实现其实是一个java.util.HashMap
支持。
HashSet
是根据对象的哈希值来确定元素在集合中的存储位置,因此具有良好的存取和查找性能。保证元素唯一性的方式依赖于:hashCode
与equals
方法。
11.8.2 HashSet存储数据结构
在JDK1.8之前,哈希表底层采用数组+链表实现,即使用链表处理冲突,同一hash值的链表都存储在一个链表里。但是当位于一个桶中的元素较多,即hash值相等的元素较多时,通过key值依次查找的效率较低。而JDK1.8中,哈希表存储采用数组+链表+红黑树实现,当链表长度超过阈值(8)时,将链表转换为红黑树,这样大大减少了查找时间。
案例
public class Student {
private String name;
private int age;
public Student() {
}
public Student(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
Student student = (Student) o;
return age == student.age &&
Objects.equals(name, student.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
}
public class HashSetDemo2 {
public static void main(String[] args) {
//创建集合对象 该集合中存储 Student类型对象
HashSet<Student> stuSet = new HashSet<Student>();
//存储
Student stu = new Student("于谦", 43);
stuSet.add(stu);
stuSet.add(new Student("郭德纲", 44));
stuSet.add(new Student("于谦", 43));
stuSet.add(new Student("郭麒麟", 23));
stuSet.add(stu);
for (Student stu2 : stuSet) {
System.out.println(stu2);
}
}
}
/*
执行结果:
Student [name=郭德纲, age=44]
Student [name=于谦, age=43]
Student [name=郭麒麟, age=23]
*/
11.8.2 LinkedHashSet集合
在HashSet下面有一个子类java.util.LinkedHashSet
,它是链表和哈希表组合的一个数据存储结构,是有序的。
public class LinkedHashSetDemo {
public static void main(String[] args) {
Set<String> set = new LinkedHashSet<String>();
set.add("bbb");
set.add("aaa");
set.add("abc");
set.add("bbc");
Iterator<String> it = set.iterator();
while (it.hasNext()) {
System.out.println(it.next());
}
}
}
/*
结果:
bbb
aaa
abc
bbc
*/
11.8.3 TreeSet集合
- 底层实现:红黑树–平衡二叉树
- 有序的(不是插入顺序,自定义顺序)
- 不重复
11.9 Collections
11.9.1 Collections常用方法
java.utils.Collections
是集合工具类,用来对集合进行操作,常用方法如下:
public static <T> boolean addAll(Collection<T> c, T... elements)
:往集合中添加一些元素。public static void shuffle(List<?> list)
打乱顺序:打乱集合顺序。public static <T> void sort(List<T> list)
:将集合中元素按照默认规则排序。public static <T> void sort(List<T> list,Comparator<? super T> )
:将集合中元素按照指定规则排序。
public class CollectionsDemo {
public static void main(String[] args) {
ArrayList<Integer> list = new ArrayList<Integer>();
//采用工具类 完成 往集合中添加元素
Collections.addAll(list, 5, 222, 1,2);
System.out.println(list);
//排序方法
Collections.sort(list);
System.out.println(list);
}
}
/*
结果:
[5, 222, 1, 2]
[1, 2, 5, 222]
*/
11.9.2 Comparator比较器
在JAVA中提供了两种比较实现的方式,一种是比较死板的采用
java.lang.Comparable
接口去实现,一种是灵活的当我需要做排序的时候在去选择的java.util.Comparator
接口完成。
当我们采用的
public static <T> void sort(List<T> list)
这个方法完成的排序,实际上要求了被排序的类型需要实现Comparable接口完成比较的功能,在String类型中如下:
public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
@Override
public int compareTo(String anotherString) {
int len1 = value.length;
int len2 = anotherString.value.length;
int lim = Math.min(len1, len2);
char v1[] = value;
char v2[] = anotherString.value;
int k = 0;
while (k < lim) {
char c1 = v1[k];
char c2 = v2[k];
if (c1 != c2) {
return c1 - c2;
}
k++;
}
return len1 - len2;
}
}
String类实现了这个接口,并完成了比较规则的定义,但是这样就把这种规则写死了,那比如我想要字符串按照第一个字符降序排列,那么这样就要修改String的源代码,这是不可能的了,那么这个时候我们可以使用
public static <T> void sort(List<T> list,Comparator<? super T> )
方法灵活的完成,这个里面就涉及到了Comparator这个接口,位于位于java.util包下,排序是comparator能实现的功能之一,该接口代表一个比较器,比较器具有可比性!顾名思义就是做排序的,通俗地讲需要比较两个对象谁排在前谁排在后,那么比较的方法就是:
public int compare(String o1, String o2)
:比较其两个参数的顺序。
两个对象比较的结果有三种:大于,等于,小于。
如果要按照升序排序,
则o1 小于o2,返回(负数),相等返回0,o1大于o2返回(正数)
如果要按照降序排序
则o1 小于o2,返回(正数),相等返回0,o1大于o2返回(负数)
public class CollectionsDemo3 {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<String>();
list.add("cba");
list.add("aba");
list.add("sba");
list.add("nba");
//排序方法 按照第一个单词的降序
Collections.sort(list, new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
return o2.charAt(0) - o1.charAt(0);
}
});
System.out.println(list);
}
}
/*
结果如下:
[sba, nba, cba, aba]
*/
11.9.3 Comparable和Comparator两个接口的区别
Comparable:强行对实现它的每个类的对象进行整体排序。这种排序被称为类的自然排序,类的compareTo方法被称为它的自然比较方法。只能在类中实现compareTo()一次,不能经常修改类的代码实现自己想要的排序。实现此接口的对象列表(和数组)可以通过Collections.sort(和Arrays.sort)进行自动排序,对象可以用作有序映射中的键或有序集合中的元素,无需指定比较器。
Comparator:强行对某个对象进行整体排序。可以将Comparator 传递给sort方法(如Collections.sort或 Arrays.sort),从而允许在排序顺序上实现精确控制。还可以使用Comparator来控制某些数据结构(如有序set或有序映射)的顺序,或者为那些没有自然顺序的对象collection提供排序。
11.10 Map集合
11.10.1 概述
Java提供了专门的集合类用来存放这种键与值对应关系的对象,即java.util.Map
接口。
Collection
中的集合,元素是孤立存在的,向集合中存储元素采用一个个元素的方式存储。Map
中的集合,元素是成对存在的,每个元素由键与值两部分组成,通过键可以找对所对应的值。Collection中
的集合称为单列集合,Map中的集合称为双列集合。- 需要注意的是,
Map
中的集合不能包含重复的键,值可以重复;每个键只能对应一个值。
11.10.3 Map接口中的常用方法
常用方法如下:
public V put(K key, V value)
: 把指定的键与指定的值添加到Map集合中。public V remove(Object key)
:把指定的键 所对应的键值对元素 在Map集合中删除,返回被删除元素的值。public V get(Object key)
:根据指定的键,在Map集合中获取对应的值。boolean containsKey(Object key)
:判断集合中是否包含指定的键。public Set<K> keySet()
:获取Map集合中所有的键,存储到Set集合中。public Set<Map.Entry<K,V>> entrySet()
:获取到Map集合中所有的键值对对象的集合(Set集合)。
使用put方法时,若指定的键(key)在集合中没有,则没有这个键对应的值,返回null,并把指定的键值添加到集合中;
若指定的键(key)在集合中存在,则返回值为集合中键对应的值(该值为替换前的值),并把指定键所对应的值,替换成指定的新值。
11.10.2 Map集合的子类
HashMap<K,V>
:存储数据采用的哈希表结构,元素的存取顺序不能保证一致。由于要保证键的唯一、不重复,需要重写键的hashCode()方法、equals()方法。LinkedHashMap<K,V>
:HashMap下有个子类LinkedHashMap,存储数据采用的哈希表结构+链表结构。通过链表结构可以保证元素的存取顺序一致;通过哈希表结构可以保证的键的唯一、不重复,需要重写键的hashCode()方法、equals()方法。
Map接口中的集合都有两个泛型变量<K,V>,在使用时,要为两个泛型变量赋予数据类型。两个泛型变量<K,V>的数据类型可以相同,也可以不同。
11.10.2.1 HashMap集合
通过键来遍历
public class MapDemo01 {
public static void main(String[] args) {
//创建Map集合对象
HashMap<String, String> map = new HashMap<String,String>();
//添加元素到集合
map.put("胡歌", "霍建华");
map.put("郭德纲", "于谦");
map.put("薛之谦", "大张伟");
//获取所有的键 获取键集
Set<String> keys = map.keySet();
// 遍历键集 得到 每一个键
for (String key : keys) {
//key 就是键
//获取对应值
String value = map.get(key);
System.out.println(key+"的CP是:"+value);
}
}
}
通过entry来遍历
public class MapDemo02 {
public static void main(String[] args) {
// 创建Map集合对象
HashMap<String, String> map = new HashMap<String,String>();
// 添加元素到集合
map.put("胡歌", "霍建华");
map.put("郭德纲", "于谦");
map.put("薛之谦", "大张伟");
// 获取 所有的 entry对象 entrySet
Set<Entry<String,String>> entrySet = map.entrySet();
// 遍历得到每一个entry对象
for (Entry<String, String> entry : entrySet) {
// 解析
String key = entry.getKey();
String value = entry.getValue();
System.out.println(key+"的CP是:"+value);
}
}
}
11.10.2.2 LinkedHashMap集合
HashMap保证成对元素唯一,并且查询速度很快,可是成对元素存放进去是没有顺序的,如果要保证有序,可以使用HashMap下面的一个子类LinkedHashMap,它是链表和哈希表组合的一个数据存储结构。
public class LinkedHashMapDemo {
public static void main(String[] args) {
LinkedHashMap<String, String> map = new LinkedHashMap<String, String>();
map.put("邓超", "孙俪");
map.put("李晨", "范冰冰");
map.put("刘德华", "朱丽倩");
Set<Entry<String, String>> entrySet = map.entrySet();
for (Entry<String, String> entry : entrySet) {
System.out.println(entry.getKey() + " " + entry.getValue());
}
}
}
/*
结果:
邓超 孙俪
李晨 范冰冰
刘德华 朱丽倩
*/
(第二部分)IO流
1.1 File类
1.1.1 概述
java.io.File
类是文件和目录路径名的抽象表示,主要用于文件和目录的创建、查找和删除等操作。
1.1.2 构造方法
-
public File(String pathname)
:通过将给定的路径名字符串转换为抽象路径名来创建新的 File实例。 -
public File(String parent, String child)
:从父路径名字符串和子路径名字符串创建新的 File实例。 -
public File(File parent, String child)
:从父抽象路径名和子路径名字符串创建新的 File实例。
一个File对象代表硬盘中实际存在的一个文件或者目录。
无论该路径下是否存在文件或者目录,都不影响File对象的创建。
1.1.3 常用方法
获取方法
-
public String getAbsolutePath()
:返回此File的绝对路径名字符串。 -
public String getPath()
:将此File转换为路径名字符串。 -
public String getName()
:返回由此File表示的文件或目录的名称。 -
public long length()
:返回由此File表示的文件的长度。
判断方法
public boolean exists()
:此File表示的文件或目录是否实际存在。public boolean isDirectory()
:此File表示的是否为目录。public boolean isFile()
:此File表示的是否为文件。
创建删除方法
-
public boolean createNewFile()
:当且仅当具有该名称的文件尚不存在时,创建一个新的空文件。 -
public boolean delete()
:删除由此File表示的文件或目录。 -
public boolean mkdir()
:创建由此File表示的目录。 -
public boolean mkdirs()
:创建由此File表示的目录,包括任何必需但不存在的父目录。
delete()方法,如果此File表示目录,则目录必须为空才能删除。
1.1.4 目录的遍历
-
public String[] list()
:返回一个String数组,表示该File目录中的所有子文件或目录。 -
public File[] listFiles()
:返回一个File数组,表示该File目录中的所有的子文件或目录。
public class FileFor {
public static void main(String[] args) {
File dir = new File("d:\\java_code");
//获取当前目录下的文件以及文件夹的名称。
String[] names = dir.list();
for(String name : names){
System.out.println(name);
}
//获取当前目录下的文件以及文件夹对象,只要拿到了文件对象,那么就可以获取更多信息
File[] files = dir.listFiles();
for (File file : files) {
System.out.println(file);
}
}
}
调用listFiles()方法的File对象,表示的必须是实际存在的目录,否则返回null,无法进行遍历。
1.2 递归
1.2.1 概述
-
递归:指在当前方法内调用自己的这种现象。
-
递归的分类:
- 递归分为两种,直接递归和间接递归。
- 直接递归称为方法自身调用自己。
- 间接递归可以A方法调用B方法,B方法调用C方法,C方法调用A方法。
-
注意事项:
- 递归一定要有条件限定,保证递归能够停止下来,否则会发生栈内存溢出。
- 在递归中虽然有限定条件,但是递归次数不能太多。否则也会发生栈内存溢出。
- 构造方法,禁止递归
1.2.2 递归打印文件目录
public class DiGuiDemo2 {
public static void main(String[] args) {
// 创建File对象
File dir = new File("D:\\aaa");
// 调用打印目录方法
printDir(dir);
}
public static void printDir(File dir) {
// 获取子文件和目录
File[] files = dir.listFiles();
// 循环打印
/*
判断:
当是文件时,打印绝对路径.
当是目录时,继续调用打印目录的方法,形成递归调用.
*/
for (File file : files) {
// 判断
if (file.isFile()) {
// 是文件,输出文件绝对路径
System.out.println("文件名:"+ file.getAbsolutePath());
} else {
// 是目录,输出目录绝对路径
System.out.println("目录:"+file.getAbsolutePath());
// 继续遍历,调用printDir,形成递归
printDir(file);
}
}
}
}
1.2.3 文件过滤器
java.io.FileFilter
是一个接口,是File的过滤器。 该接口的对象可以传递给File类的listFiles(FileFilter)
作为参数, 接口中只有一个方法。
boolean accept(File pathname)
:参数为File,表示当前File下所有的子文件和子目录。保留住则返回true,过滤掉则返回false。
public class DiGuiDemo4 {
public static void main(String[] args) {
File dir = new File("D:\\aaa");
printDir2(dir);
}
public static void printDir2(File dir) {
// 匿名内部类方式,创建过滤器子类对象,不过滤.java文件,如果是目录的话就继续递归
File[] files = dir.listFiles(new FileFilter() {
@Override
public boolean accept(File pathname) {
return pathname.getName().endsWith(".java")||pathname.isDirectory();
}
});
// lambda的改写
/*
File[] files = dir.listFiles(f ->{
return f.getName().endsWith(".java") || f.isDirectory();
});
*/
// 循环打印
for (File file : files) {
if (file.isFile()) {
System.out.println("文件名:" + file.getAbsolutePath());
} else {
printDir2(file);
}
}
}
}
1.3 字节流和字符流
1.3.1 IO概述
Java中IO操作主要是指使用java.io
包下的内容,进行输入、输出操作。输入也叫做读取数据,输出也叫做作写出数据。
1.3.1.1 IO的分类
根据数据的流向分为:输入流和输出流。
- 输入流 :把数据从其他设备上读取到内存中的流。
- 输出流 :把数据从内存 中写出到其他设备上的流。
根据数据的类型分为:字节流和字符流。
- 字节流 :以字节为单位,读写数据的流。
- 字符流 :以字符为单位,读写数据的流。
输入流 | 输出流 | |
---|---|---|
字节流 | InputStream | OutputStream |
字符流 | InputStream | Writer |
1.3.2 字节流
1.3.2.1 概述
一切文件数据(文本、图片、视频等)在存储时,都是以二进制数字的形式保存,都一个一个的字节,那么传输时一样如此。所以,字节流可以传输任意文件数据。在操作流的时候,我们要时刻明确,无论使用什么样的流对象,底层传输的始终为二进制数据。
1.3.2.2 字节输入流
java.io.InputStream
抽象类是表示字节输入流的所有类的超类,可以读取字节信息到内存中。它定义了字节输入流的基本共性功能方法。
public void close()
:关闭此输入流并释放与此流相关联的任何系统资源。public abstract int read()
: 从输入流读取数据的下一个字节。public int read(byte[] b)
: 从输入流中读取一些字节数,并将它们存储到字节数组 b中 。
close()方法,当完成流的操作时,必须调用此方法,释放系统资源。
1.3.2.3 FileInputStream类
java.io.FileInputStream
:是文件输入流,从文件中读取字节。
构造方法
FileInputStream(File file)
: 通过打开与实际文件的连接来创建一个 FileInputStream ,该文件由文件系统中的 File对象 file命名。FileInputStream(String name)
: 通过打开与实际文件的连接来创建一个 FileInputStream ,该文件由文件系统中的路径名 name命名。
当你创建一个流对象时,必须传入一个文件路径。该路径下,如果没有该文件,会抛出FileNotFoundException
。
public class FISRead {
public static void main(String[] args) throws IOException{
// 使用文件名称创建流对象
FileInputStream fis = new FileInputStream("read.txt");
// 定义变量,保存数据
int b ;
// 循环读取
while ((b = fis.read())!=-1) {
System.out.println((char)b);
}
// 关闭资源
fis.close();
}
}
/*
输出结果:
a
b
c
d
e
*/
虽然读取了一个字节,但是会自动提升为int类型。
public class FISRead {
public static void main(String[] args) throws IOException{
// 使用文件名称创建流对象.
FileInputStream fis = new FileInputStream("read.txt"); // 文件中为abcde
// 定义变量,作为有效个数
int len ;
// 定义字节数组,作为装字节数据的容器
byte[] b = new byte[2];
// 循环读取
while (( len= fis.read(b))!=-1) {
// 每次读取后,把数组的有效字节部分,变成字符串打印
System.out.println(new String(b,0,len));// len 每次读取的有效字节个数
}
// 关闭资源
fis.close();
}
}
/*
输出结果:
ab
cd
e
*/
使用数组读取,每次读取多个字节,减少了系统间的IO操作次数,从而提高了读写的效率,建议开发中使用。
1.3.2.4 字节输出流
java.io.OutputStream
:抽象类是表示字节输出流的所有类的超类,将指定的字节信息写出到目的地。它定义了字节输出流的基本共性功能方法。
public void close()
:关闭此输出流并释放与此流相关联的任何系统资源。public void flush()
:刷新此输出流并强制任何缓冲的输出字节被写出。public void write(byte[] b)
:将 b.length字节从指定的字节数组写入此输出流。public void write(byte[] b, int off, int len)
:从指定的字节数组写入 len字节,从偏移量 off开始输出到此输出流。public abstract void write(int b)
:将指定的字节输出流。
close()方法,当完成流的操作时,必须调用此方法,释放系统资源。
1.3.2.5 FileOutputStream类
java.io.FileOutputStream
:是文件输出流,用于将数据写出到文件。
构造方法
public FileOutputStream(File file)
:创建文件输出流以写入由指定的 File对象表示的文件。public FileOutputStream(String name)
: 创建文件输出流以指定的名称写入文件。
当你创建一个流对象时,必须传入一个文件路径。该路径下,如果没有这个文件,会创建该文件。如果有这个文件,会清空这个文件的数据。
public class FOSWrite {
public static void main(String[] args) throws IOException {
// 使用文件名称创建流对象
FileOutputStream fos = new FileOutputStream("fos.txt");
// 写出数据
fos.write(97); // 写出第1个字节
fos.write(98); // 写出第2个字节
fos.write(99); // 写出第3个字节
// 关闭资源
fos.close();
}
}
/*
文件内容:
abc
*/
虽然参数为int类型四个字节,但是只会保留一个字节的信息写出。
public class FOSWrite {
public static void main(String[] args) throws IOException {
// 使用文件名称创建流对象
FileOutputStream fos = new FileOutputStream("fos.txt");
// 字符串转换为字节数组
byte[] b = "abcde".getBytes();
// 写出从索引2开始,2个字节。索引2是c,两个字节,也就是cd。
fos.write(b,2,2);
// 关闭资源
fos.close();
}
}
/*
输出结果:
cd
*/
文件内容的追加
public FileOutputStream(File file, boolean append)
: 创建文件输出流以写入由指定的 File对象表示的文件。public FileOutputStream(String name, boolean append)
: 创建文件输出流以指定的名称写入文件。
这两个构造方法,参数中都需要传入一个boolean类型的值,true
表示追加数据,false
表示清空原有数据。
public class FOSWrite {
public static void main(String[] args) throws IOException {
// 使用文件名称创建流对象
FileOutputStream fos = new FileOutputStream("fos.txt",true);
// 字符串转换为字节数组
byte[] b = "abcde".getBytes();
// 写出从索引2开始,2个字节。索引2是c,两个字节,也就是cd。
fos.write(b);
// 关闭资源
fos.close();
}
}
/*
文件操作前:cd
文件操作后:cdabcde
*/
1.3.4 字符流
1.3.4.1 概述
当使用字节流读取文本文件时,可能会有一个小问题。就是遇到中文字符时,可能不会显示完整的字符,那是因为一个中文字符可能占用多个字节存储。所以Java提供一些字符流类,以字符为单位读写数据,专门用于处理文本文件。
1.3.4.2 字符输入流
java.io.Reader
:抽象类是表示用于读取字符流的所有类的超类,可以读取字符信息到内存中。它定义了字符输入流的基本共性功能方法。
public void close()
:关闭此流并释放与此流相关联的任何系统资源。public int read()
: 从输入流读取一个字符。public int read(char[] cbuf)
: 从输入流中读取一些字符,并将它们存储到字符数组 cbuf中 。
1.3.4.3 FileReader类
java.io.FileReader
:类是读取字符文件的便利类。构造时使用系统默认的字符编码和默认字节缓冲区(8192字节)。
- 字符编码:字节与字符的对应规则。Windows系统的中文编码默认是GBK编码表。
idea中UTF-8- 字节缓冲区:一个字节数组,用来临时存储字节数据。
构造方法
FileReader(File file)
: 创建一个新的 FileReader ,给定要读取的File对象。FileReader(String fileName)
: 创建一个新的 FileReader ,给定要读取的文件的名称。
public class FRRead {
public static void main(String[] args) throws IOException {
// 使用文件名称创建流对象
FileReader fr = new FileReader("read.txt");
// 定义变量,保存数据
int b ;
// 循环读取
while ((b = fr.read())!=-1) {
System.out.println((char)b);
}
// 关闭资源
fr.close();
}
}
/*
输出结果:
a
b
c
d
e
*/
虽然读取了一个字符,但是会自动提升为int类型。
public class FRRead {
public static void main(String[] args) throws IOException {
// 使用文件名称创建流对象
FileReader fr = new FileReader("read.txt");
// 定义变量,保存有效字符个数
int len ;
// 定义字符数组,作为装字符数据的容器
char[] cbuf = new char[2];
// 循环读取
while ((len = fr.read(cbuf))!=-1) {
System.out.println(new String(cbuf));
}
// 关闭资源
fr.close();
}
}
/*
输出结果:
ab
cd
ed
*/
每次读取时会覆盖之前读取的内容,当最后不够是,会输出之前读取的内容。
1.3.4.4 字符输出流
java.io.Writer
:抽象类是表示用于写出字符流的所有类的超类,将指定的字符信息写出到目的地。它定义了字节输出流的基本共性功能方法。
void write(int c)
写入单个字符。void write(char[] cbuf)
写入字符数组。abstract void write(char[] cbuf, int off, int len)
写入字符数组的某一部分,off数组的开始索引,len写的字符个数。void write(String str)
写入字符串。void write(String str, int off, int len)
写入字符串的某一部分,off字符串的开始索引,len写的字符个数。void flush()
刷新该流的缓冲。void close()
关闭此流,但要先刷新它。
1.3.4.5 FileWriter类
java.io.FileWriter
:类是写出字符到文件的便利类。构造时使用系统默认的字符编码和默认字节缓冲区。
构造方法
FileWriter(File file)
: 创建一个新的 FileWriter,给定要读取的File对象。FileWriter(String fileName)
: 创建一个新的 FileWriter,给定要读取的文件的名称。
public class FWWrite {
public static void main(String[] args) throws IOException {
// 使用文件名称创建流对象
FileWriter fw = new FileWriter("fw.txt");
// 写出数据
fw.write(97); // 写出第1个字符
fw.write('b'); // 写出第2个字符
fw.write('C'); // 写出第3个字符
fw.write(30000); // 写出第4个字符,中文编码表中30000对应一个汉字。
/*
【注意】关闭资源时,与FileOutputStream不同。
如果不关闭,数据只是保存到缓冲区,并未保存到文件。
*/
// fw.close();
}
}
/*
输出结果:
abC田
*/
- 虽然参数为int类型四个字节,但是只会保留一个字符的信息写出。
- 未调用close方法,数据只是保存到了缓冲区,并未写出到文件中。
因为内置缓冲区的原因,如果不关闭输出流,无法写出字符到文件中。但是关闭的流对象,是无法继续写出数据的。如果我们既想写出数据,又想继续使用流,就需要flush
方法了。
flush
:刷新缓冲区,流对象可以继续使用。close
:先刷新缓冲区,然后通知系统释放资源。流对象不可以再被使用了。
public class FWWrite {
public static void main(String[] args) throws IOException {
// 使用文件名称创建流对象
FileWriter fw = new FileWriter("fw.txt");
// 写出数据,通过flush
fw.write('刷'); // 写出第1个字符
fw.flush();
fw.write('新'); // 继续写出第2个字符,写出成功
fw.flush();
// 写出数据,通过close
fw.write('关'); // 写出第1个字符
fw.close();
fw.write('闭'); // 继续写出第2个字符,【报错】java.io.IOException: Stream closed
fw.close();
}
}
- 即便是flush方法写出了数据,操作的最后还是要调用close方法,释放系统资源。
- 字符流,只能操作文本文件,不能操作图片,视频等非文本文件。
- 当我们单纯读或者写文本文件时 使用字符流 其他情况使用字节流
1.3.5 属性集
1.3.5.1 概述
java.util.Properties
继承于Hashtable
,来表示一个持久的属性集。它使用键值结构存储数据,每个键及其对应值都是一个字符串。该类也被许多Java类使用,比如获取系统属性时,System.getProperties
方法就是返回一个Properties
对象。
1.3.5.2 Properties类
构造方法
public Properties()
:创建一个空的属性列表。
基本的存储方法
public Object setProperty(String key, String value)
: 保存一对属性。public String getProperty(String key)
:使用此属性列表中指定的键搜索属性值。public Set<String> stringPropertyNames()
:所有键的名称的集合。
public class ProDemo {
public static void main(String[] args) throws FileNotFoundException {
// 创建属性集对象
Properties properties = new Properties();
// 添加键值对元素
properties.setProperty("filename", "a.txt");
properties.setProperty("length", "209385038");
properties.setProperty("location", "D:\\a.txt");
// 打印属性集对象
System.out.println(properties);
// 通过键,获取属性值
System.out.println(properties.getProperty("filename"));
System.out.println(properties.getProperty("length"));
System.out.println(properties.getProperty("location"));
// 遍历属性集,获取所有键的集合
Set<String> strings = properties.stringPropertyNames();
// 打印键值对
for (String key : strings ) {
System.out.println(key+" -- "+properties.getProperty(key));
}
}
}
/*
输出结果:
{filename=a.txt, length=209385038, location=D:\a.txt}
a.txt
209385038
D:\a.txt
filename -- a.txt
length -- 209385038
location -- D:\a.txt
*/
与流相关的方法
public void load(Reader reader)
:从字符输入流中读取键值对。public void load(InputStream inStream)
: 从字节输入流中读取键值对。public void store(Writer writer, String comments)
:从字符输出流中写入键值对。public void store(OutputStream out, String comments)
:从字节输出流中写入键值对。
public class PropertiesTest {
public static void main(String[] args) {
save();
get();
}
private static void get() {
Properties prop=new Properties();
try {
// prop.load(new FileInputStream("stu.properties"));
prop.load(new InputStreamReader(new FileInputStream("stu.properties"),"utf-8"));
System.out.println(prop.getProperty("1001"));
} catch (IOException e) {
e.printStackTrace();
}
}
private static void save() {
Properties prop=new Properties();
prop.setProperty("1001", "张三");
prop.setProperty("1002", "李四");
prop.setProperty("1003", "wangwu");
try {
// prop.store(new FileOutputStream("stu.properties"), null);
prop.store(new OutputStreamWriter(new FileOutputStream("stu.properties"),"utf-8") , null);
} catch (IOException e) {
e.printStackTrace();
}
}
}
1.3.6 缓冲流
1.3.6.1 概述
缓冲流,也叫高效流,是对4个基本的FileXxx 流的增强,所以也是4个流,按照数据类型分类:
- 字节缓冲流:
BufferedInputStream,BufferedOutputStream
- 字符缓冲流:
BufferedReader,BufferedWriter
缓冲流的基本原理,是在创建流对象时,会创建一个内置的默认大小的缓冲区数组,通过缓冲区读写,减少系统IO次数,从而提高读写的效率。
1.3.6.2 字节缓存流
构造方法
public BufferedInputStream(InputStream in)
:创建一个 新的缓冲输入流。public BufferedOutputStream(OutputStream out)
: 创建一个新的缓冲输出流。
public class BufferedDemo {
public static void main(String[] args) throws FileNotFoundException {
// 记录开始时间
long start = System.currentTimeMillis();
// 创建流对象
try (
BufferedInputStream bis = new BufferedInputStream(new FileInputStream("jdk9.exe"));
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("copy.exe"));
){
// 读写数据
int len;
byte[] bytes = new byte[8*1024];
while ((len = bis.read(bytes)) != -1) {
bos.write(bytes, 0 , len);
}
} catch (IOException e) {
e.printStackTrace();
}
// 记录结束时间
long end = System.currentTimeMillis();
System.out.println("缓冲流使用数组复制时间:"+(end - start)+" 毫秒");
}
}
//缓冲流使用数组复制时间:666 毫秒
1.3.6.3 字符缓冲流
构造方法
public BufferedReader(Reader in)
:创建一个 新的缓冲输入流。public BufferedWriter(Writer out)
: 创建一个新的缓冲输出流。
特有方法
BufferedReader:public String readLine()
: 读一行文字。BufferedWriter:public void newLine()
: 写一行行分隔符,由系统属性定义符号。
public class BufferedReaderDemo {
public static void main(String[] args) throws IOException {
// 创建流对象
BufferedReader br = new BufferedReader(new FileReader("in.txt"));
// 定义字符串,保存读取的一行文字
String line = null;
// 循环读取,读取到最后返回null
while ((line = br.readLine())!=null) {
System.out.print(line);
System.out.println("------");
}
// 释放资源
br.close();
}
}
1.3.7 转换流
1.3.7.1 字符编码和字符集
字符编码
计算机中储存的信息都是用二进制数表示的,而我们在屏幕上看到的数字、英文、标点符号、汉字等字符是二进制数转换之后的结果。按照某种规则,将字符存储到计算机中,称为编码 。反之,将存储在计算机中的二进制数按照某种规则解析显示出来,称为解码 。比如说,按照A规则存储,同样按照A规则解析,那么就能显示正确的文本符号。反之,按照A规则存储,再按照B规则解析,就会导致乱码现象。
编码:字符(能看懂的)–字节(看不懂的)
解码:字节(看不懂的)–>字符(能看懂的)
- 字符编码
Character Encoding
:就是一套自然语言的字符与二进制数之间的对应规则。 - 编码表:生活中文字和计算机中二进制的对应规则
字符集
- 字符集
Charset
:也叫编码表。是一个系统支持的所有字符的集合,包括各国家文字、标点符号、图形符号、数字等。
计算机要准确的存储和识别各种字符集符号,需要进行字符编码,一套字符集必然至少有一套字符编码。常见字符集有ASCII字符集、GBK字符集、Unicode字符集等。
可见,当指定了编码,它所对应的字符集自然就指定了,所以编码才是我们最终要关心的。
- GBK 编码中,中⽂字符占 2 个字节,英⽂字符占 1 个字节;
- UTF-8 编码中,中⽂字符占 3 个字节,英⽂字符占 1 个字节;
- UTF-16 编码中,中⽂字符和英⽂字符都占 2 个字节。
1.3.7.2 InputStreamReader类
转换流java.io.InputStreamReader
,是Reader的子类,是从字节流到字符流的桥梁。它读取字节,并使用指定的字符集将其解码为字符。它的字符集可以由名称指定,也可以接受平台的默认字符集。
构造方法
InputStreamReader(InputStream in)
: 创建一个使用默认字符集的字符流。
InputStreamReader(InputStream in, String charsetName)
: 创建一个指定字符集的字符流。
public class ReaderDemo2 {
public static void main(String[] args) throws IOException {
// 定义文件路径,文件为gbk编码
String FileName = "E:\\file_gbk.txt";
// 创建流对象,默认UTF8编码
InputStreamReader isr = new InputStreamReader(new FileInputStream(FileName));
// 创建流对象,指定GBK编码
InputStreamReader isr2 = new InputStreamReader(new FileInputStream(FileName) , "GBK");
// 定义变量,保存字符
int read;
// 使用默认编码字符流读取,乱码
while ((read = isr.read()) != -1) {
System.out.print((char)read); // ��Һ�
}
isr.close();
// 使用指定编码字符流读取,正常解析
while ((read = isr2.read()) != -1) {
System.out.print((char)read);// 大家好
}
isr2.close();
}
}
1.3.7.3 OutputStreamWriter类
转换流java.io.OutputStreamWriter
,是Writer的子类,是从字符流到字节流的桥梁。使用指定的字符集将字符编码为字节。它的字符集可以由名称指定,也可以接受平台的默认字符集。
构造方法
OutputStreamWriter(OutputStream in)
: 创建一个使用默认字符集的字符流。OutputStreamWriter(OutputStream in, String charsetName)
: 创建一个指定字符集的字符流。
public class OutputDemo {
public static void main(String[] args) throws IOException {
// 定义文件路径
String FileName = "E:\\out.txt";
// 创建流对象,默认UTF8编码
OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream(FileName));
// 写出数据
osw.write("你好"); // 保存为6个字节
osw.close();
// 定义文件路径
String FileName2 = "E:\\out2.txt";
// 创建流对象,指定GBK编码
OutputStreamWriter osw2 = new OutputStreamWriter(new FileOutputStream(FileName2),"GBK");
// 写出数据
osw2.write("你好");// 保存为4个字节
osw2.close();
}
}
1.3.8 序列化
Java 提供了一种对象序列化的机制。用一个字节序列可以表示一个对象,该字节序列包含该对象的数据
、对象的类型
和对象中存储的属性
等信息。字节序列写出到文件之后,相当于文件中持久保存了一个对象的信息。
反之,该字节序列还可以从文件中读取回来,重构对象,对它进行反序列化。对象的数据、对象的类型和对象中存储的数据信息,都可以用来在内存中创建对象。看图理解序列化:
1.3.8.1 ObjectOutputStream类
java.io.ObjectOutputStream
类,将Java对象的原始数据类型写出到文件,实现对象的持久存储。
构造方法
public ObjectOutputStream(OutputStream out)
: 创建一个指定OutputStream的ObjectOutputStream。
序列化操作
一个对象要想序列化,必须满足两个条件:
1. 该类必须实现java.io.Serializable 接口,Serializable 是一个标记接口,不实现此接口的类将不会使任何状态序列化或反序列化,会抛出NotSerializableException 。
2. 该类的所有属性必须是可序列化的。如果有一个属性不需要可序列化的,则该属性必须注明是瞬态的,使用transient 关键字修饰。
public class Employee implements java.io.Serializable {
public String name;
public String address;
public transient int age; // transient瞬态修饰成员,不会被序列化
public void addressCheck() {
System.out.println("Address check : " + name + " -- " + address);
}
}
public class SerializeDemo{
public static void main(String [] args) {
Employee e = new Employee();
e.name = "zhangsan";
e.address = "beiqinglu";
e.age = 20;
try {
// 创建序列化流对象
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("employee.txt"));
// 写出对象
out.writeObject(e);
// 释放资源
out.close();
System.out.println("Serialized data is saved"); // 姓名,地址被序列化,年龄没有被序列化。
} catch(IOException i) {
i.printStackTrace();
}
}
}
/*
输出结果:
Serialized data is saved
*/
注意点
- 父类实现了序列化,子类可以继承,无需再实现此接口
- 静态的属性也是不序列化的,静态的内容是属于类的
1.3.8.2 ObjectInputStream类
ObjectInputStream反序列化流,将之前使用ObjectOutputStream序列化的原始数据恢复为对象。
构造方法
public ObjectInputStream(InputStream in)
: 创建一个指定InputStream的ObjectInputStream。
反序列化操作1
如果能找到一个对象的class文件,我们可以进行反序列化操作,调用ObjectInputStream读取对象的方法:
public final Object readObject ()
: 读取一个对象。
public class DeserializeDemo {
public static void main(String [] args) {
Employee e = null;
try {
// 创建反序列化流
FileInputStream fileIn = new FileInputStream("employee.txt");
ObjectInputStream in = new ObjectInputStream(fileIn);
// 读取一个对象
e = (Employee) in.readObject();
// 释放资源
in.close();
fileIn.close();
}catch(IOException i) {
// 捕获其他异常
i.printStackTrace();
return;
}catch(ClassNotFoundException c) {
// 捕获类找不到异常
System.out.println("Employee class not found");
c.printStackTrace();
return;
}
// 无异常,直接打印输出
System.out.println("Name: " + e.name); // zhangsan
System.out.println("Address: " + e.address); // beiqinglu
System.out.println("age: " + e.age); // 0
}
}
对于JVM可以反序列化对象,它必须是能够找到class文件的类。如果找不到该类的class文件,则抛出一个 ClassNotFoundException
异常。
反序列化操作2
另外,当JVM反序列化对象时,能找到class文件,但是class文件在序列化对象之后发生了修改,那么反序列化操作也会失败,抛出一个InvalidClassException
异常。发生这个异常的原因如下:
- 该类的序列版本号与从流中读取的类描述符的版本号不匹配
- 该类包含未知数据类型
- 该类没有可访问的无参数构造方法
Serializable
接口给需要序列化的类,提供了一个序列版本号。serialVersionUID
该版本号的目的在于验证序列化的对象和对应类是否版本匹配,主要用在序列化列升级的时候(属性和方法有改动的时候)。
public class Employee implements java.io.Serializable {
// 加入序列版本号
private static final long serialVersionUID = 1L;
public String name;
public String address;
// 添加新的属性 ,重新编译, 可以反序列化,该属性赋为默认值.
public int eid;
public void addressCheck() {
System.out.println("Address check : " + name + " -- " + address);
}
}
1.3.8.3 序列化集合
public class SeriaTest {
public static void main(String[] args) {
// 程序启动的时候,从文件加载
ArrayList<Vechile> vlist2 = load();
for (Vechile vechile : vlist2) {
System.out.println(vechile);
}
ArrayList<Vechile> vList = new ArrayList<Vechile>();
vList.add(new Vechile("陕A66666", "宝马", 1));
vList.add(new Vechile("陕B88888", "奔驰", 1));
vList.add(new Vechile("陕A99999", "东风", 2));
// 程序退出的时候,保存
save(vList);
}
private static ArrayList<Vechile> load() {
File f = new File("vlist.data");
if (!f.exists())
return new ArrayList<Vechile>();
ArrayList<Vechile> vList = null;
ObjectInputStream ois = null;
try {
ois = new ObjectInputStream(new FileInputStream("vlist.data"));
vList = (ArrayList<Vechile>) ois.readObject();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} finally {
try {
if (ois != null) {
ois.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
return vList;
}
private static void save(ArrayList<Vechile> vList) {
ObjectOutputStream oos = null;
try {
oos = new ObjectOutputStream(new FileOutputStream("vlist.data"));
oos.writeObject(vList);
} catch (IOException e) {
e.printStackTrace();
} finally {
if (oos != null) {
try {
oos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
1.3.9 数据流和打印流
1.3.9.1 数据流
java.io.DataInputStream
:用于读取二进制文件和java基本数据类型 。
java.io.DataOutputStream
:用于操作二进制文件和java基本数据类型 。
构造方法
public DataInputStream(InputStream in)
: 使用指定的输入流创建一个新的数据流输入流。public DataOutputStream(OutputStream out)
:使用指定的输入流创建一个新的数据流输出流。
public class DataTest {
public static void main(String[] args) throws IOException {
write();
read();
}
private static void read() throws IOException {
DataInputStream dis=new DataInputStream(new FileInputStream("data.data"));
System.out.println(dis.readInt());
System.out.println(dis.readBoolean());
System.out.println(dis.readUTF());
System.out.println(dis.readUTF());
}
private static void write() throws IOException {
//创建流
DataOutputStream dos=new DataOutputStream(new FileOutputStream("data.data"));
//写
dos.writeInt(48);
dos.writeBoolean(true);
dos.writeUTF("你好");
dos.writeUTF("今天天气不错");
//关闭流
dos.close();
}
}
1.3.9.2 打印流
1.3.9.2.1 概述
平时我们在控制台打印输出,是调用print
方法和println
方法完成的,这两个方法都来自于java.io.PrintStream
类,该类能够方便地打印各种数据类型的值,是一种便捷的输出方式。
1.3.9.2.2 PrintStream类
构造方法
public PrintStream(String fileName)
: 使用指定的文件名创建一个新的打印流。
改变打印流向
System.out
就是PrintStream
类型的,只不过它的流向是系统规定的,打印在控制台上。不过,既然是流对象,我们就可以改变它的流向。
public class PrintDemo {
public static void main(String[] args) throws IOException {
// 调用系统的打印流,控制台直接输出97
System.out.println(97);
// 创建打印流,指定文件的名称
PrintStream ps = new PrintStream("ps.txt");
// 设置系统的打印流流向,输出到ps.txt
System.setOut(ps);
// 调用系统的打印流,ps.txt中输出97
System.out.println(97);
}
}