Java基础

70 阅读36分钟

Java基础

初识Java

特点:

  • 一次编译,到处运行(半编译半解释型语言,先通过javac编译为字节码文件,再由JVM解释为机器码)
  • 相比C++简单,没有指针语法,只有引用,更健壮,有垃圾回收机制
  • 面向对象,不支持多重继承,使用接口概念进行实现
  • 分布式,有丰富的例程库处理HTTP,FTP等TCP/IP协议
  • 可移植性,Java的基本数据类型大小都做了明确说明,固定不变,不存在代码移植时平台不同而造成的错误
  • 高性能,边解释边执行,垃圾回收,使用JIT即时编译器,热点代码探测提高执行效率
  • 多线程, 支持并发程序设计
  • 动态性,支持反射找到运行时类型信息

安装:

环境变量:

为什么配置环境变量? 观察发现在执行文件目录底下敲命令可以执行,而在其他目录下不可执行, 所以环境变量就是一个查找的路径, 可以告诉命令行要在哪里找。 如果环境变量修改了 一定要重启命令行 或者使用source命令。

MacOS Java开发环境安装

Windows Java开发环境安装

Java程序

Java程序的运行

Java是一门半编译型、半解释型语言。

先通过javac编译程序把源文件进行编译,编译后生成的.class文件是由字节码组成的平台无关、面向JVM的文件。

最后启动java虚拟机来运行.class文件,此时JVM会将字节码转换成平台能够理解的形式来运行。

JDK里面就包含了javac和java工具,Java程序最终是在JVM(Java虚拟机)中运行的

JDK、JRE、JVM之间的关系?
  • JDK(Java Development Kit): Java开发工具包,提供给Java程序员使用,包含了JRE,同时还包含了编译 器javac与自带的调试工具Jconsole、jstack等。
  • JRE(Java Runtime Environment): Java运行时环境,包含了JVM,Java基础类库。是使用Java语言编写程 序运行的所需环境。
  • JVM:Java虚拟机,运行.class字节码文件

截屏2024-09-13 16.45.22

程序编写规范
  • 类名 -- 大驼峰命名 方法--小驼峰命名 对象名--小驼峰命名
JavaDoc
  • Javadoc命令编译之后会自动生成网页 网页的内容根据 @params等注释解释
编码
  • 编写代码的时候 要注意编码格式 utf-8还是gbk 可以在编译选项处加上 -encoding utf-8
  • Java字符类型采用unicode编码方案 每个unicode码占用两个字节 两个字节--》16个比特位 也就是使用两个字节来表示一个字符 前128个字符是ASCII码 后面拓展码
  • acsii码 与 unicode码不同 为单字节编码 只占用一个字节

数据类型与变量

基本数据类型

四类八种

四类: 整型,浮点型,字符型,布尔型

八种:

截屏2024-09-13 16.51.21

  • Java中没有无符号类型 符号位不属于数值位
  • 字面常量默认的类型: 整型int 浮点double 注意没有加f的小数都默认为double类型
  • int 无论在何种系统上都是4个字节 设置初始值时,值不能超过int的表示范围,否则会导致溢出
  • 在 Java 中, int 除以 int 的值仍然是 int(会直接舍弃小数部分)
  • C 语言中使用 ASCII 表示字符, 而 Java 中使用 Unicode 表示字符. 因此一个字符占用两个字节, 表示的字符种类更多, 包括中文.
  • Java 的 boolean 类型和 int 不能相互转换, 不存在 1 表示 true, 0 表示 false 这样的用法
类型转换:
自动类型转换(隐式)

数据范围小的转为数据范围大的时会自动进行。

强制类型转换(显式)

强制类型转换:当进行操作时,代码需要经过一定的格式处理,不能自动完成。特点:数据范围大的到数据范围小的,可能精度丢失,不相干的类型无法转换。

类型提升:
  • int与long之间:int会被提升为long,不同类型的数据混合运算, 范围小的会提升成范围大的.
  • byte与byte的运算,会先提升为int(比int小的都是)进行原酸,得到的结果是int,需要强制转换才能赋值给一个byte类型变量

原因:

由于计算机的 CPU 通常是按照 4 个字节为单位从内存中读写数据. 为了硬件上实现方便, 诸如 byte 和 short这种低于 4 个字节的类 型, 会先提升成 int, 再参与计算.

字符串类型:

  • String类, 引用类型
  • Java的String不会以'\0'结尾 底层就是一个数组
  • String与基本数据类型的转换: String到基本数据类型-》对应包装类型的parse方法,基本数据类型到String-> String.valueOf()创建对象
  • Java中 如果 字符串和整数 中有 + 为拼接符, 拼接字符串
  • num+"" 为字符串

运算符

  • 运算符之间是有优先级的. 具体的规则我们不必记忆. 在可能存在歧义的代码中加上括号即可.

基本操作符:

  • Int/int -> 向下取整
  • 两侧操作数类型不一致时,向类型大的提升 1+0.2 1会提升为double

增量操作符

该种类型运算符操作完成后,会将操纵的结果赋值给左操作数

int a = 4;
double d = 5.0;
a+=d; //对于+=操作符来说 自动给你进行转换

自增/减操作符

前置++ 先+1,然后使用变量+1之后的值,后置++ 先使用变量原来的值,表达式结束时给变量+1

逻辑运算符

&& 和 || 遵守短路求值的规则.

  • 对于 && , 如果左侧表达式值为 false, 则表达式结果一定是 false, 无需计算右侧表达式.
  • 对于 ||, 如果左侧表达式值为 true, 则表达式结果一定是 true, 无需计算右侧表达式.

位运算符

  • 按位取反 ~: 如果该位为 0 则转为 1, 如果该位为 1 则转为 0

  • 按位异或 ^: 如果两个数字的二进制位相同, 则结果为 0, 相异则结果为 1. 如果两个数相同,则异或的结果为0

    应用: 找只出现了一次的元素 可以直接使用异或运算符 异或运算符中相同的元素进行运算结果为0 0 与 任何元素异或等于那个元素 所以一直异或下去就可以得到那个单独的值

  • 无符号右移 >>>: 最右侧位不要了, 最左侧补 0

  • 没有无符号左移这种运算符!!!

应用:
  1. 左移 1 位, 相当于原数字 * 2. 左移 N 位, 相当于原数字 * 2 的N次方.
  2. 右移 1 位, 相当于原数字 / 2. 右移 N 位, 相当于原数字 / 2 的N次方.
  3. 由于计算机计算移位效率高于计算乘除, 当某个代码正好乘除 2 的N次方的时候可以用移位运算代替

易错,三目运算符各部分遵循就近原则,所以第一个?前面就是表达式部分,第一个:后面的整体就是条件表达式为false时对应的部分,那么这个表达式的值实际上为false

程序逻辑控制

Switch

switch的括号内只能是以下类型的表达式:

  • 基本类型:byte、char、short、int,注意不能是long类型
  • 引用类型:String常量串、枚举类型

方法

  • 在Java中,实参的值永远都是拷贝到形参中,形参和实参本质是两个实体
  • 对于基础类型来说, 形参相当于实参的拷贝. 即 传值调用、
  • 在参数列表加上... 表示可变参数 int...a 与 int[]数组类型等价

解决Swap交换问题

传引用类型参数 (例如数组来解决这个问题),数组中的位置互换

方法重载

如果多个方法的名字相同,参数列表不同,则称该几种方法被重载了。与返回值类型是否相同无关

方法签名:

经过编译器编译修改过之后方法最终的名字。具体方式:方法全路径名+参数列表+返回值类型,构成方法完整的名字。

数组(以[开头,配合其他的特殊字符,表述对应数据类型的数组,几个[表述几维数组)

引用类型,以L开头,以;结尾,中间是引用类型的全类名

递归

  1. 将原问题划分成其子问题,注意:子问题必须要与原问题的解法相同 2. 确定递归出口
  • 方法调用的时候, 会有一个 "栈" 这样的内存空间描述当前的调用关系. 称为调用栈. 每一次的方法调用就称为一个 "栈帧", 每个栈帧中包含了这次调用的参数是哪些, 返回到哪里继续执行等信息
  • 递归对空间的要求比较高
  • 迭代--》循环

数组

创建方法

T[] 数组名 = new T[N];

初始化

动态初始化

int[] array = new int[10];

静态初始化

T[] 数组名称 = {data1, data2, data3, ..., datan};

如果数组中存储元素类型为基类类型,默认值为基类类型对应的默认值

截屏2024-09-13 17.21.27

  • 在数组中可以通过 数组对象.length 来获取数组的长度

数组是引用类型

JVM的内存分布:

Java虚拟机栈,堆,Java本地方法栈,方法区,程序计数器

截屏2024-09-13 17.23.02

  • 虚拟机栈(JVM Stack): 与方法调用相关的一些信息,每个方法在执行时,都会先创建一个栈帧,栈帧中包含 有:局部变量表、操作数栈、动态链接、返回地址以及其他的一些信息,保存的都是与方法执行时相关的一 些信息。比如:局部变量。当方法运行结束后,栈帧就被销毁了,即栈帧中保存的数据也被销毁了。
  • 堆(Heap): JVM所管理的最大内存区域. 使用 new 创建的对象都是在堆上保存 (例如前面的 new int[]{1, 2, 3} ),堆是随着程序开始运行时而创建,随着程序的退出而销毁,堆中的数据只要还有在使用,就不会被销 毁。
  • 方法区(Method Area): 用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数 据. 方法编译出的的字节码就是保存在这个区域
引用数据类型

截屏2024-09-13 17.26.08

  • 引用变量并不直接存储对象本身,可以简单理解成存储的是对象在堆中空间的起始地址。通过该地址,引用变量便可以去操作对象
  • 当一个对象没有引用指向的时候就会被系统自动回收
  • null 在 Java 中表示 "空引用" , 也就是一个不指向对象的引用.一旦尝试读写, 就会抛出 NullPointerException
  • 如果将引用传入方法之后又重新开辟内存 那么这个参数不再引用原来的空间 此时引用的是新的空间 方法销毁时 不会改变原来形参的值
  • 引用可以避免对整个数组的拷贝(数组可能比较长, 那么拷贝开销就会很大).

数组转字符串

Arrays工具累 在java.util下

Arrays.toString() 打印数组

Arrays.sort() 排序数组

Arrays.equals 判断两个数组的内容是否一致

Arrays.fill(arr,9) 将数组里面所有的元素都填充为9

Arrays.copyOf(arr1,arr1.length) 可以拷贝 也可以进行扩容操作 第二个参数为指定的数组长度

copyOfRange 指定范围进行拷贝

System.arraycopy(原数组, 原数组下标, 目的数组,目的数组的位置,拷贝的长度) 是native方法(底层是由C,C++实现, 特点是速度很快) 将引用赋值给别的数据类型不是拷贝

二维数组

二维数组本质上也就是一维数组, 只不过每个元素又是一个一维数组.

数据类型 数组名称 = new 数据类型 [行数][列数] { 初始化数据 }

Java当中的二维数组(2x3) 真正是两个元素为引用类型 指向了两个不同的一维数组(不同的行)

int arr = new[2][]; 只指定了为两个引用类型的数组 但引用类型没有指向的对象 所以两个引用变量的值都为null

可以分别初始化和赋值 所以这个数组是可以不规则的

类与对象

  • 面向对象(OOP)-》 主要依靠对象之间的交互完成一件事情
  • 类对一个实体(对象)进行描述

类的定义

  • 大驼峰命名类
class className{
  fields;
  methods;
}
  • 用new关键字实例化对象

this引用

this引用指向当前对象(成员方法运行时调用该成员方法的对象),在成员方法中所有成员变量的操作,都是通过该 引用去访问。编译器自动完成传递this引用

  1. this的类型:对应类类型引用,即哪个对象调用就是哪个对象的引用类型
  2. this只能在"成员方法"中使用
  3. 在"成员方法"中,this只能引用当前对象,不能再引用其他对象

构造方法

  1. 名字必须与类名相同
  2. 没有返回值类型,设置为void也不行
  3. 创建对象时由编译器自动调用,并且在对象的生命周期内只调用一次(相当于人的出生,每个人只能出生一次)
  4. 构造方法可以重载(用户根据自己的需求提供不同参数的构造方法)
  5. 编译器会默认生成一个不带参数的构造方法。如果有构造方法,那么编译器将不会再生成不带参数的构造方法
  • 构造方法中,可以通过this调用其他构造方法来简化代码,比如无参构造可以讲默认值传递到有参构造的方法中

  • this(...)必须是构造方法中的第一条语句,用this调用构造方法也只能在构造方法中出现

    原因: this代表的是当前对象的引用,只有在调用构造完成之后才产生了对象,所以this代表的是当前对象的引用(new产生)

  • 绝大多数情况下使用public来修饰,特殊场景下会被private修饰(单例模式)

new

当new一个对象时,JVM层面发生了

  1. 检测对象对应的类是否加载了,如果没有加载则加载
  2. 为对象分配内存空间
  3. 处理并发安全问题(两个对象同时创建,要看分配的内存空间是否会冲突)
  4. 初始化所分配的空间
  5. 设置对象头信息
  6. 调用构造方法,给对象中各个成员赋值

面向对象

  • 三大特性: 封装,继承, 多态
  • 封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行 交互
  • Java中主要通过类和访问权限来实现封装:类可以将数据以及封装数据的方法结合在一起,访问权限用来控制方法或者字段能否直接在类外使用

截屏2024-09-13 21.00.06

  • default 什么都不写时的默认权限
  • 一般情况下成员变量设置为private,成员方法设置为public。
  • 为了更好的管理类,把多个类收集在一起成为一组,称为软件包。有点类似于目录。

  • 包是对类、接口等的封装机制的体现,是一种对类或者接口等的很好的组织方式,比如一个包中的类不想被其他包中的类使用。

  • 导包: import java.util.*; *为通配符,表示该包下的所有类都可以使用, 但最好还是显式指定包中的哪个类,避免不同包下的类重名造成冲突, 用的时候才会导入 用哪个拿哪个 不会像C++全部导入,import 更类似于 C++ 的 namespace 和 using,比如写全类名的时候也是可以正常运行的

  • 自定义包:

    1. 在文件的最上方加上一个 package 语句指定该代码在哪个包中.
    2. 包名需要尽量指定成唯一的名字, 通常会用公司的域名的颠倒形式(例如 com.bit.demo1 )
  1. 如果一个类没有 package 语句, 则该类被放到一个默认包中
  • 常见包;

    1. java.lang:系统常用基础类(String、Object),此包从JDK1.1后自动导入。
    2. java.lang.reflect:java 反射编程包;
    3. java.net:进行网络编程开发包。
    4. java.sql:进行数据库开发的支持包。
    5. java.util:是java提供的工具程序包。(集合类等) 非常重要
    6. java.io:I/O编程开发包

static

  • 静态成员变量 --》 类变量
  • 静态成员方法 --》 类方法
成员:

在Java中,被static修饰的成员,称之为静态成员,也可以称为类成员,其不属于某个具体的对象,是所有对象所共享的。

  • 可以通过类名进行访问
  • 存储在方法区当中
方法:

Java中,被static修饰的成员方法称为静态成员方法,是类的方法,不是某个对象所特有的。静态成员一般是通过静态方法来访问的。

  • 静态方法当中不能调用非静态的成员变量或者是非静态的成员方法, 因为其他的成员变量和方法都依赖实例化创建,不包含this 因为对象都没有创建
  • 可以通过对象调用,也可以通过类名.静态方法名(...)方式调用,更推荐使用后者
静态成员通过静态代码块进行初始化

普通代码块 构造块 静态块 同步代码块(后续讲解多线程部分再谈)

  1. 构造块(实例代码块):在类中,成员方法外
{
  this.name = "bit";
  this.age = 12;
  this.sex = "man";
  System.out.println("I am instance init()!");
}

2. 静态代码块: 初始化静态成员变量

static{
  classRoom = "bit306";
  System.out.println("I am static init()!");
}
  • 静态代码块不管生成多少个对象,其只会执行一次
  • 静态成员变量是类的属性,因此是在JVM加载类时开辟空间并初始化的
  • 如果一个类中包含多个静态代码块,在编译代码时,编译器会按照定义的先后次序依次执行(合并)
  • 实例代码块只有在创建对象时才会执行
  • 静态代码块先执行 然后才是实例代码块 然后才是构造方法

内部类

在 Java 中,可以将一个类定义在另一个类或者一个方法的内部,前者称为内部类,后者称为外部类。内部类也是封装的一种体现。

public class OutClass {
  class InnerClass{
  
  }
}
  • 内部类和外部类共用同一个java源文件,但是经过编译之后,内部类会形成单独的字节码文件
实例内部类
  1. 外部类中的任何成员都可以在实例内部类方法中直接访问
  2. 实例内部类所处的位置与外部类成员位置相同,因此也受public、private等访问限定符的约束
  3. 在实例内部类方法中访问同名的成员时,优先访问自己的,如果要访问外部类同名的成员,必须:外部类名 称.this.同名成员 来访问
  4. 实例内部类对象必须在先有外部类对象前提下才能创建
  5. 实例内部类的非静态方法中包含了一个指向外部类对象的引用
  6. 外部类中,不能直接访问实例内部类中的成员,如果要访问必须先要创建内部类的对象
静态内部类

被static修饰的内部成员类称为静态内部类

在静态内部类中只能访问外部类中的静态成员, 如果实在要访问,实例化外部类对象再进行访问

创建静态内部类对象时,不需要先创建外部类对象

匿名内部类

直接new 实现一个接口 然后重写接口里面的方法 还可以直接在new对象之后直接调用

对象的打印

重写toString()方法即可 默认Object类中的toString方法输出对象的地址

继承和多态

继承

  • Java中只存在单继承, 不存在多继承, 尽量不要超过三层的继承关系

借助extends关键字 子类定义时 extends 父类

继承是一种思想 对共性进行抽取 从而实现了代码的复用

继承之后,子类可以复用父类中成员,子类在实现时只需关心自己新增加的成员即可。

截屏2024-09-13 21.33.16

  1. 子类会将父类中的成员变量或者成员方法继承到子类中了
  2. 子类继承父类之后,必须要新添加自己特有的成员,体现出与基类的不同,否则就没有必要继承了
  • 如果访问的成员变量子类中有,优先访问自己的成员变量。(就近原则)
  • 如果访问的成员变量子类中无,则访问父类继承下来的,如果父类也没有定义,则编译报错。
  • 如果访问的成员变量与父类中成员变量同名,则优先访问自己的。
  • 区分相同名称的成员是子类的(this.) 还是父类的(super.)

通过派生类对象访问父类与子类同名方法时,如果父类和子类同名方法的参数列表不同(重载),根据调用 方法适传递的参数选择合适的方法访问

super关键字的使用
  1. 只能在非静态方法中使用
  2. 在子类方法中,访问父类的成员变量和方法。
  • 子类对象构造时,需要先调用基类构造方法,然后执行子类的构造方法
  • 默认会先调用super() --》父类的无参构造 父类的成员需要初始化 并不会生成父类对象
  • super(...)不能与this(...)同时使用
super与this的比较
  1. this是当前对象的引用,当前对象即调用实例方法的对象,super相当于是子类对象中从父类继承下来部分成 员的引用
  2. 在非静态成员方法中,this用来访问本类的方法和属性,super用来访问父类继承下来的方法和属性
  3. 在构造方法中:this(...)用于调用本类构造方法,super(...)用于调用父类构造方法,两种调用不能同时在构造 方法中出现
  4. 构造方法中一定会存在super(...)的调用,用户没有写编译器也会增加,但是this(...)用户不写则没有
父类子类初始化顺序

静态代码块是在类加载阶段就执行, 所以一定是比其他阶段都要早的, 所以是父类与子类的静态代码块先执行, 然后才是父类的实例代码块先执行,默认第一句super(...),然后就到父类的构造方法执行, 然后才是子类的实例代码块和构造方法执行

protected

主要用于继承关系上

截屏2024-09-14 00.18.55

通过super进行访问, 不同包中父类的成员变量, 要在非static方法内进行访问

修饰词选择:

类要尽量做到 "封装", 即隐藏内部实现细节, 只暴露出 必要 的信息给类的调用者

我们在使用的时候应该尽可能的使用 比较严格 的访问权限. 例如如果一个方法能用 private, 就尽量不要用public

final

修饰变量

修饰成员方法:

修饰类: 表示该类不可以再被继承,比如String

修饰变量或者字段: 表示常量,不可修改

修饰方法: 表示该方法不能被重写

继承与组合

继承表示 is-a 组合表示has-a

一般来说, 能用组合尽量还是用组合, 组合用的更多

组合可以面向接口编程,也就是可以在运行时才决定对象内部的字段,是一种松耦合的设计,而继承是一种紧耦合的设计(子类依赖父类实现)

一个判断方法是,问一问自己是否需要从新类向基类进行向上转型。如果是必须的,则继承是必要的。反之则应该好好考虑是否需要继承。(需要两个类确实存在is-a的关系)

多态

  • 具体点就是去完成某个行为,当不同的对象去完成时会产生出不同 的状态。

截屏2024-09-14 00.32.59

同一件事情,发生在不同对象身上就会产生不同的结果

条件
  1. 必须在继承体系下(才能发生向上转型)
  2. 子类必须要对父类中方法进行重写
  3. 通过父类的引用调用重写的方法

完成以上三个部分 就会发生动态绑定,动态绑定是实现多态的基础, 在代码运行时,当传递不同类对象时,会调用对应类中的方法。

  • 子类对象 给 父类引用赋值 --》 向上转型(小范围自动提升为大范围)
  • 父类对象 给 子类 --》 向下转型(大范围强制转换为小范围),因为一个父类可能会有许多的子类,如果转换失败,就会导致程序不安全, 因此通常搭配instanceof使用,来确定该对象是不是具体子类的实例, 如果是才进行转换
  • 通过父类引用 只能调用父类自己特有的成员方法或者成员变量

常见的可以发生向上转型的时机 :

  1. 直接赋值(将子类对象赋值给父类引用)
  2. 方法的参数 , 传参的时候进行向上转型(形式参数是父类,传递的实际参数是子类)
  3. 返回值 向上转型(返回的是子类 但又赋值给父类引用)

重写(在继承关系里)

  1. 方法的返回值一样
  2. 方法名一样
  3. 方法的参数列表一样
  4. 用@override注解修饰,可以进行合法性校验
  5. 设计原则: 对于已经投入使用的类,尽量不要进行修改。最好的方式是:重新定义一个新的类,来重复利用其中共性的内容, 并且添加或者改动新的内容。
  • 重写eat方法之后 变成了调用子类的eat, 这个过程就叫做动态绑定(程序运行的时候才会选择具体的对象的具体方法)
  • 此时 a这个引用调用 eat方法可能会有多种不同的表现(和 a 引用的实例相关), 这种行为就称为 多态.

静态绑定:也称为前期绑定(早绑定),即在编译时,根据用户所传递实参类型就确定了具体调用那个方法。典型代 表函数重载。(会根据参数类型与个数进行匹配,编译时期发生) 动态绑定:也称为后期绑定(晚绑定),即在编译时,不能确定方法的行为,需要等到程序运行时,才能够确定具体 调用那个类的方法。(运行时期发生)

多态的优点: 避免大量if-else语句,代码可拓展性变强

避免在构造方法中调用重写方法

父类中如果调用了一个重写的方法, 子类的实例尚未创建,可能会造成程序异常

截屏2024-09-14 00.46.43

所以在构造函数内,尽量避免使用实例方法,除了final和private方法。因为这两个修饰词修饰的方法无法被继承

尽量不要在构造器中调用方法(如果这个方法被子类重写, 就会触发动态绑定, 但是此时子类对象还没构造完成), 可能会出现一些隐藏的但是又极难发现的问题

抽象类和接口

抽象类

  1. 使用abstract修饰的方法 叫做抽象方法,没有方法体
  2. 使用abstract修饰的类 叫做抽象类,只要包含抽象方法的类必须都是抽象类
  3. 抽象类不可以进行实例化 不能描述一个对象(不是一个具体的类,信息不够)
  4. 抽象类除此之外与普通类没有区别 一样可以定义成员变量 成员方法(可以被继承来实例化和调用)
  5. 当一个普通类 继承 抽象类 需要重写所有的抽象方法(因为抽象方法没有方法体)
  6. 抽象类的出现 就是为了被继承
  7. abstarct 和 final 不能同时出现 相互矛盾 一个要求必须被继承 一个严禁被继承
  8. 抽象方法不能是 private 的 不然将无法被继承之后重写,那么这个方法将毫无意义
  9. 抽象类中不一定包含抽象方法,但是有抽象方法的类一定是抽象类
  10. 抽象类中可以有构造方法,供子类创建对象时,初始化父类的成员变量

使用抽象类相当于多了一层编译器的校验,实际工作的处理应该由子类完成

接口

接口其实就是公共的行为规范标准,实现的时候只要符合规范标准就可以通用

在Java中,接口事多个类的公共规范,是一种引用数据类型,是一种标准

接口的定义格式与定义类的格式基本相同,将class关键字换成 interface 关键字,就定义了一个接口。

接口的命名规范
  • 接口的命名一般以大写字母I开头以区分接口
  • 接口的命名一般使用形容词词性的单词,以表示特定的功能标准
  • 阿里编码规范中约定, 接口中的方法和属性不要加任何修饰符号, 保持代码的简洁性. 直接就用默认的public abstract修饰即可,编译器会自动加上
  • 默认成员变量都是public static final修饰的 意味着是一个常量
  • 接口不能进行实例化(是抽象类的进一步抽象)
  • 类 和 接口之间的关系 使用 implement实现接口 必须实现接口当中的所有方法(重写)
  • 与普通类一样,也可以类向上转型为接口,从而发生动态绑定
  • 接口也是有对应的字节码文件的
  • 重写接口中方法时,不能使用默认的访问权限(接口中方法的权限是public, 子类的访问权限必须要大于父类,否则动态绑定会发生错误,所以重写的方法访问权限只能是public)
  • 用static和default(注意接口中的方法编译器会默认加public abstract修饰符,所以此处要强调default修饰,default关键字要写上去)修饰的时候 接口中可以实现方法, static是可以直接通过接口类型调用方法,default修饰的方法实现的类可以调用,但如果出现同时实现的两个接口中有同名方法,则必须对方法体进行重写
  • 接口中不能有静态代码块和构造方法, 静态变量为常量不可改变,接口不可以实例化对象,构造方法无意义
  • 类没有实现接口中的所有抽象方法的话就只能是抽象类

修饰顺序: 定义类时,先是继承extends 再是实现implements 可以实现多个接口(以此解决Java不可多重继承的问题)

Java 面向对象编程中最常见的用法: 一个类继承一个父类, 同时实现多种接口. 时刻牢记多态的好处, 让程序猿忘记类型. 有了接口之后, 类的使用者就不必关注具体类型,而只关注某个类是否具备某种能力

接口之间的继承

接口可以继承接口,达到复用的效果,相当于把多个接口合并在一起, 接口是可以多继承的

比较接口

CompareTo 类里面实现,对类的侵入性强,一旦写好了,就只能用这种方式比较

Comparator 内部类,临时实现比较的规则,灵活比较,传入两个要比较的对象即可

一般情况下 自定义的类型 一定是可以进行比较的 最基本的一个功能

Arrays.sort的两个重载方法,一个参数为数组, 一个参数为数组+比较器接口类(自己临时实现Comparator接口,指定比较规则)

Clonable接口,深拷贝
Clonable接口与Clone方法
  • Object 类中存在一个 clone 方法, 调用这个方法可以创建一个对象的 "拷贝". 但是要想合法调用 clone 方法, 必须要先实现 Clonable 接口, 否则会抛出CloneNotSupportedException异常
  • Object类里面的clone方法 是protected限定符修饰 Object位于java.lang包中 而自定义类型在自己的包当中 自定义类型继承Object类(属于类外继承 所以访问的限定符为protected), 所以这里需要super.方法才能进行调用(protected修饰的方法或者成员是无法直接访问的)
  • native修饰 --》 clone方法的底层由C/C++ 实现 可能会抛出异常(不支持克隆异常)
  • clone方法返回值为Object 调用成功返回结果需要自己进行强制转换为自定义类型
  • 克隆是创建了一个新的对象,复制内容,并对引用进行返回
  • 自定义类型需要支持clonable接口才能够成功调用克隆方法而不发生异常
  • Clonable 接口 的 内容是空的 --》标记接口/空接口 --》 作用: 表明当前类是可以被克隆的,由此使用clone方法时才可以被调用
  • 实现自定义类型可以被克隆调用 --》 在自定义类中重写Object类中的clone方法 在方法当中直接用super关键字调用Object类的clone方法 返回强制转换后的自定义类型对象即可
浅拷贝
  • Clonable拷贝的对象是一份浅拷贝,只拷贝了一层引用(不会把引用指向的对象空间也进行拷贝)
深拷贝
  • 深拷贝 --》把对象里面引用的空间也复制一份
  • 要实现深拷贝 里面的引用的自定义类型也要实现Clonable接口 然后重写Clone方法

典型易错题,传递给方法的参数是一份拷贝,所以过程是str接受了ex.str的拷贝,然后将形式参数str引用的空间修改为"test ok"引用的空间,但是在方法结束之后这个str形式参数被销毁,而原来的ex.str未受到影响

抽象类与接口的区别(重点)
  • 抽象类可以包含普通方法与普通字段(可以被子类继承使用),其与普通类的区别仅仅是包含了空方法体的抽象方法,子类必须重写所有的抽象方法
  • 接口 是抽象方法+全局常量的组合使用 权限只能是public

Object

  • Java中的顶级父类,所有引用类型默认继承Object类
toString方法
  • 打印类的信息,默认打印对象地址
equals方法
  • Object中默认是按照地址比较的,后续可以自己重写比较的规则
hashcode方法
  • 底层是由C/C++代码写的,也是native方法,速度很快,返回值是个内存地址
  • 所以两个对象的默认的hash值是不一样的, 当然也可以重写hashcode方法,设定获取成员字段集合的hash值,由此便可以通过hash值来比较对象的内容是否是一致的
  • 事实上hashCode() 在散列表中才有用,在其它情况下没用。在散列表中hashCode() 的作用是获取对象的散列码,进而确定该对象在散列表中的位置。

阶段项目

面向对象初级实操--图书管理系统命令行版

思路: 将增删改查操作看为对象,统一用一套规范(接口),对图书集合进行操作

String类

  • Java中字符串为单独的String类
  • 面试常问,重点内容,StringBuilder,StringBuffer,String的区别,String不可变,StringBuilder和StringBuffer可变,但是StringBuffer是线程安全的

String

  • 构造:1. 常量串直接赋值引用 2.构造方法传入常量串 3. 用字符数组构造

  • “”字面常量串也是String类型

  • 引用类型,不存储字符串本身,引用的空间内部有一个value字符数组,一个hash值

  • == 比较的是字符串的地址, equals方法比较的是字符串的内容

  • compareTo按照字典序比较,如果前面都相同,最后比较长度,compareToIgnoseCase忽略大小写进行比较

  • 字符串查找 --》KMP算法(挖坑)实现,相关方法:charAt(int),indexOf(Char/String),lastIndexOf(Char/String)

  • 字符串转化(基本类型与字符串--》xxx.parsexxx,String.valueOf(xxx), 大写 toUpperCase, 小写转toLowerCase, 数组toxxxArray, 格式化String.format)

  • 字符串替换 replaceAll/First(regex,replacement)

  • 拆分spilt(regex)

    • 字符"|","*","+"都得加上转义字符,前面加上 "\" .

    1. 而如果是 "" ,那么就得写成 "\\" .
    2. 如果一个字符串中有多个分隔符,可以用"|"作为连字符.
  • 字符串截取substring(前闭后开)

  • trim() 去掉左右空格

字符串不可变
  • String被final修饰不可继承

  • private修饰value字符数组 外部不可修改,final修饰表面引用的字符数组地址不可变

  • 因为字符串不可变,所以所有修改字符串内容的操作都是新建一个新的String对象

  • 为什么 String 要设计成不可变的?(不可变对象的好处是什么?) (选学)

    1. 方便实现字符串对象池. 如果 String 可变, 那么对象池就需要考虑写时拷贝的问题了.
    2. 不可变对象是线程安全的.(不必考虑多线程同时进行对象操作的不一致性)
    3. 不可变对象更方便缓存 hash code, 作为 key 时可以更高效的保存到 HashMap 中.
字符串修改的高效实现
  • 直接对String进行大量修改会产生大量新的对象,每次创建和销毁对象都涉及大量的资源浪费,(比如+=操作,每次都创建一个新的StringBuilder对象append之后返回toString), 千万不要在IO内每次都对String操作!!!
  • 我们在对String操作之前用String初始化一个StringBuilder对象,每次操作都操作这个StringBuilder对象,那么最后我们就只创建了一个StringBuilder,不会涉及大量的资源浪费
  • StringBuffer的方法都有sychronized修饰,线程安全,同一时刻只有一个线程可以对对象进行操作,但如果只是单一线程操作,就像上厕所,每次开锁和上锁都会造成资源消耗,那么时间效率也不如StringBuilder

异常

异常体系结构:

截屏2024-09-15 15.42.32

  1. Throwable:是异常体系的顶层类,其派生出两个重要的子类, Error 和 Exception
  2. Error:指的是Java虚拟机无法解决的严重问题,比如:JVM的内部错误、资源耗尽等,典型代表:StackOverflowError和OutOfMemoryError(栈溢出错误,内存耗尽错误),一旦发生回力乏术。
  3. Exception:异常产生后程序员可以通过代码进行处理,使程序继续执行。比如:感冒、发烧。我们平时所说的异常就是Exception。

编译时异常--》受检查异常

运行时异常--〉RuntimeException及其所有子类

事后认错型处理异常:EAFP(遇到问题再处理) throw、try、catch、final、throws

异常抛出之后 后面的代码不会执行

throws :当前方法不处理异常,提醒方法的调用者处理异常, 调用声明抛出异常的方法时,调用者必须对该异常进行处理,或者继续使用throws抛出

try{
// 将可能出现异常的代码放在这里
}catch(要捕获的异常类型 e){
// 如果try中的代码抛出异常了,此处catch捕获时异常类型与try中抛出的异常类型一致时,或者是try中抛出异常的基类
时,就会被捕获到
// 对异常就可以正常处理,处理完成后,跳出try-catch结构,继续执行后序代码
}[catch(异常类型 e){//多个异常也可以用|符号分隔
// 对异常进行处理
}finally{
// 此处代码一定会被执行到
}]

如果抛出异常类型与catch时异常类型不匹配,即异常不会被成功捕获,也就不会被处理,继续往外抛,直到JVM收到后中断程序----异常是按照类型来捕获的

如果异常之间具有父子关系,一定是子类异常在前catch,父类异常在后catch,这样也能起到殿后的作用

finally

不管try catch语句是否捕获到异常,最后一定会执行finally里面的代码

应用: 多用于程序打开资源的释放,防止资源泄漏,比如文件,网络,数据库连接

也可以不用自己手写资源的close,可以采用try()里面打开资源的方式,执行完语句之后会自动释放资源

try(Scanner sc = new Scanner(System.in)){
  int a = sc.nextInt();
}catch(RuntimeException ex){
  ex...
}

finally无论try是否成功,或者在try中发生了返回语句,都会执行,有效防止了资源泄漏

自定义异常类

自定义的业务异常 要确定是否需要进行编译时检查(是否为受查异常),从而确定是继承Exception(编译时异常)还是RuntimeException(运行时异常)

另外,自定义异常时要实现一个带有String参数的构造方法,阐述异常产生的原因