这是一门神奇的语言
数据类型 变量名称
例子:int a;
小的整数类型可传递到大的整数类型
表示一个long类型的常量数字,需要在后面添加大写或者小写的L才行
long a = 1333333333333333l;
十六进制:0x
八进制:0
表示一个float类型的常量数字,需要在后面添加大写或者小写的f才行 float b= 15.5f;
隐式转换规则:byte→short(char)→int→long→float→double (可能会出现丢失精度的情况)
ASCII码:每一个字符对应一个数字
字符常量值: 需要单引号(‘’),并且内部只能有一个字符 char c = ‘A’;
字符串类型(对象类型): 需要双引号(“”),字符串可以包含多个字符 String C = “ABC”;
boolean d = true; (无视大小写)
引用数据类型与基本数据类型的区别:
通过图例来说明:
可以看到 a , b ,arr ,都是在main方法中创建的局部变量,所以他们都在栈空间上
通过图例看看引用变量是如何操作对象的:
因为array2和array1中存放的都是两个数组的地址,所以当这行代码执行完后array1就不再指向原来的绿色的数组了,array1也指向蓝色的数组
所以array1通过下标,array2通过下标都可以操作蓝色的数组
不同类型的整数一起运算,小类型需要转换为大类型 :short、byte、char一律转换为int再进行计算(无论算式中有无int,都需要转换),结果也是int;
加法运算支持对字符串的拼接:String str = “jiang” + “zheng” + “真帅” + 0; 自动转换为字符
两个整数在进行除法运算时,得到的结果也是整数 (会直接砍掉小数部分,注意不是四舍五入)
作为强制类型转换 short h = (short) f; 做到大类型转换为小类型
a++ 与 ++a 的区别:
与=结合:a+=1 等价 a = a+1
位运算符:& | ^ ~
位移运算符:<< >>
逻辑运算符:&& || !;返回类型为boolean布尔类型
三元运算符:判断语句 ? 结果1 : 结果2→上面的判断为真,返回结果1;判断为假,返回结果2
例子:a > 10 ? ‘A’ : ‘B’;
用于判断
if (条件判断) 判断成功执行的代码
注意:
只要两个分支的判断语句
最后当之前所有的if都判断失败时,才会进入到最后的else语句中
用于匹配,多分支情况:
switch (目标) { //我们需要传入一个目标,比如变量,或是计算表达式等
case 匹配值: //如果目标的值等于我们这里给定的匹配值,那么就执行case后面的代码
代码...
break; //代码执行结束后需要使用break来结束,否则会溜到下一个case继续执行代码
default: 其他情况下执行的代码
}
通常switch中可以继续嵌套其他的流程控制语句:
public static void main(String[] args) {
char c = 'A';
switch (c) {
case 'A':
if(c == 'A') { //嵌套一个if语句
System.out.println("去尖子班!");
}
break;
case 'B':
System.out.println("去平行班!");
break;
}
}
for循环更多用于明确知道循环的情况:
for (表达式1;表达式2;表达式3) 循环体;
标准的for循环语句:
public static void main(String[] args) {
for (int i = 0; i < 3; i++) //这里我们在for语句中定义一个变量i,然后每一轮i都会自增,直到变成3为止
System.out.println("伞兵一号卢本伟准备就绪!"); //这样,就会执行三轮循环,每轮循环都会执行紧跟着的这一句打印
}
注意这里的 i 仅仅是for循环语句中创建的变量,所以说其作用域被限制在了循环体中,一旦离开循环体,那么就无法使用了;但可以在外部进行定义;
两层for循环:
public static void main(String[] args) {
for (int i = 0; i < 3; i++) //外层循环执行3次
for (int j = 0; j < 3; j++) //内层循环也执行3次
System.out.println("1!5!");
}
无限for循环:
public static void main(String[] args) {
for (;;) //如果什么都不写,相当于没有结束条件,这将会导致无限循环
System.out.println("伞兵一号卢本伟准备就绪!");
}
while循环更多的用在不明确具体的结束时机的情况下,即不知道循环次数:
while(循环条件) 循环体;
第一种:标准的while循环语句:先做循环判断,再做执行内容
public static void main(String[] args) {
int i = 100; //比如现在我们想看看i不断除以2得到的结果会是什么,但是循环次数我们并不明确
while (i > 0) { //现在唯一知道的是循环条件,只要大于0那么就可以继续除
System.out.println(i);
i /= 2; //每次循环都除以2
}
}
第二种:do-while循环语句:先执行内容,然后再做循环条件判断
public static void main(String[] args) {
int i = 0; //比如现在我们想看看i不断除以2得到的结果会是什么,但是循环次数我们并不明确
do { //无论满不满足循环条件,先执行循环体里面的内容
System.out.println("Hello World!");
i++;
} while (i < 10); //再做判断,如果判断成功,开启下一轮循环,否则结束
}
加速循环继续
public static void main(String[] args) {
for (int i = 0; i < 3; i++) {
if(i == 1) continue; //比如我们希望当i等于1时跳过这一轮,不执行后面的打印
System.out.println("伞兵一号卢本伟准备就绪!");
System.out.println("当前i的值为:"+i);
}
}
使用continue关键字来跳过本轮循环,直接开启下一轮。这里的跳过是指在循环体中,无论后面有没有未执行的代码,一律不执行;
提前终止整个循环
for (int i = 0; i < 3; i++) {
if(i == 1) break; //我们希望当i等于1时提前结束
System.out.println("伞兵一号卢本伟准备就绪!");
System.out.println("当前i的值为:"+i);
}
break和continue关键字能够更方便的控制循环,但是注意在多重循环嵌套下,它只对离它最近的循环生效(就近原则)
如果想要终止或者是加速外层循环呢?我们可以为循环语句打上标记:
outer: for (int i = 1; i < 4; ++i) { //在循环语句前,添加 标签: 来进行标记
inner: for (int j = 1; j < 4; ++j) {
if(i == j) break outer; //break后紧跟要结束的循环标记,当i == j时终止外层循环
System.out.println(i+", "+j);
}
}
在类中创建类的成员变量
public class Person { //这里定义的人类具有三个属性,名字、年龄、性别
String name; //直接在类中定义变量,表示类具有的属性
int age;
String sex;
}
之后在通过new关键字,创建一个具体的对象
public static void main(String[] args) {
new Person(); //我们可以使用new关键字来创建某个类的对象,注意new后面需要跟上 类名()
//这里创建出来的,就是一个具体的人了
}
使用一个变量来指代某个对象,只不过引用类型的变量,存储的是对象的引用,而不是对象本身
public static void main(String[] args) {
//创建一个变量指代我们刚刚创建好的对象,变量的类型就是对应的类名
//这里的p存放的是对象的引用,而不是本体,我们可以通过对象的引用来间接操作对象
//这里也将类的内容看作是物品,new的对象p相当于袋子,整体上意思就是提物品,需要用袋子装
Person p = new Person();
}
变量p2赋值为p1的值,那么实际上只是传递了对象的引用,而不是对象本身的复制
public static void main(String[] args) {
Person p1 = new Person();
Person p2 = p1; //传递对象的引用,并且引用的是同一个对象
}
public static void main(String[] args) {
Person p1 = new Person(); //这两个变量分别引用的是不同的两个对象
Person p2 = new Person();
System.out.println(p1 == p2); //如果两个变量存放的是不同对象的引用
}
修改对象的操作
public static void main(String[] args) {
Person p = new Person();
p.name = "小明"; //要访问对象的属性,我们需要使用 . 运算符
System.out.println(p.name); //直接打印对象的名字,就是我们刚刚修改好的结果了
}
不对任何对象进行引用
public static void main(String[] args) {
Person p1 = null; //null是一个特殊的值,它表示空,也就是不引用任何的对象
}
方法的定义:在类中定义;在主函数中执行
返回值类型 方法名称() { 方法体... }
public class Person { //类中定义
String name;
int age;
String sex; //自我介绍只需要完成就行,没有返回值,所以说使用
void void hello(){ //完成自我介绍需要执行的所有代码就在这个花括号中编写
//这里编写代码跟我们之前在main中是一样的(实际上main就是一个函数)
//自我介绍需要用到当前对象的名字和年龄,我们直接使用成员变量即可,变量的值就是当前对象的存放值
System.out.println("我叫 "+name+" 今年 "+age+" 岁了!");
}
}
方法的调用
public static void main(String[] args) { //主函数中执行
Person p = new Person();
p.name = "小明";
p.age = 18;
p.hello(); //我们只需要使用 . 运算符,就可以执行定义好的方法了,只需要 .方法名称() 即可
}
我们的方法需要别人提供参与运算的值才可以;我们可以为方法设定参数,在调用方法时,需要外部传入参数才可以
int sum(int a, int b){
int c = a + b;
return c; //return后面紧跟需要返回的结果,这样就可以将计算结果丢出去了
//带返回值的方法,是一定要有一个返回结果的!否则无法通过编译!
}
public static void main(String[] args) {
Person p = new Person();
p.name = "小明";
p.age = 18;
int result = p.sum(10, 20); //现在我们要让这个对象帮我们计算10 + 20的结果
System.out.println(result); //成功得到30,实际上这里的println也是在调用方法进行打印操作
}
注意的细节:
void test(int a){
if(a == 10) return; //当a等于10时直接结束方法,后面无论有没有代码都不会执行了
System.out.println("Hello World!"); //不是的情况就正常执行
}
void swap(int a, int b){ //这个函数的目的很明显,就是为了交换a和b的值
int tmp = a;
a = b;
b = a;
}
public static void main(String[] args) {
Person p = new Person();
int a = 5,
b = 9; //外面也叫a和b
p.swap(a, b);
System.out.println("a = "+a+", b = "+b); //最后的结果会变成什么样子呢?
}
两者a,b值没有发生交换,我们交换的仅仅是方法中的a和b,参数传递仅仅是值传递,我们是没有办法直接操作到外面的a和b的。,真正需要是地址传递
出现与成员变量重名的变量,想要在方法中访问到当前对象的属性,使用this关键字
void setName(String name) {
this.name = name; //让当前对象的name变量值等于参数传入的值
}
一个类中可以包含多个同名的方法,但是需要的形式参数不同,方法的返回类型可以相同,也可以不同;但是仅仅返回类型不同是不允许的
int sum(int a, int b){ //必须要做到形式参数不同
return a + b;
}
double sum(double a, double b){ //为了支持小数加法,我们可以进行一次重载
return a + b;
}
方法之间的相互调用或者自己调用自己(递归)
void test(){
System.out.println("我是test"); //实际上这里也是调用另一个方法
}
void say(){
test(); //在一个方法内调用另一个方法
}
void test(){ //自己调用自己
test();
}
在对象创建时就为其指定内容,就要在对象创建时进行处理,可以使用构造方法(构造器)
来完成
构造方法不需要填写返回值,并且方法名称与类名相同,默认情况下每个类都会自带一个没有任何参数的无参构造方法(只是不用我们去写,编译出来就自带),当然,我们也可以手动声明,对其进行修改:
public class Person {
String name;
int age;
String sex;
Person(){ //构造方法不需要指定返回值,并且方法名称与类名相同
name = "小明"; //构造方法会在对象创建时执行,我们可以将各种需要初始化的操作都在这里进行处理
age = 18;
sex = "男";
}
}
构造方法中设定参数,需要用this关键字,并且定义一个构造方法之后,会覆盖掉默认的那一个无参构造方法:
public class Person {
String name;
int age;
String sex;
Person(String name, int age, String sex){ //跟普通方法是一样的
this.name = name;
this.age = age;
this.sex = sex;
}
}
//主方法
public static void main(String[] args) {
Person p = new Person("小明", 18, "男"); //调用自己定义的带三个参数的构造方法
System.out.println(p.name);
}
代码块的定义,会在最一开始执行:
{
System.out.println("我是代码块"); //代码块中的内容会在对象创建时仅执行一次
}
静态的内容,是属于这个类的。可以理解为所有对象共享的内容;
我们通过使用static关键字来声明一个变量或一个方法为静态的,一旦被声明为静态,那么通过这个类创建的所有对象,操作的都是同一个目标,也就是说,对象再多,也只有这一个静态的变量或方法。
一个对象改变了静态变量的值,那么其他的对象读取的就是被改变的值。
静态变量
public class Person {
String name;
int age;
String sex;
static String info; //这里我们定义一个info静态变量
}
//主方法
public static void main(String[] args) {
Person p1 = new Person();
Person p2 = new Person();
p1.info = "杰哥你干嘛";
System.out.println(p2.info); //可以看到,由于静态属性是属于类的,因此无论通过什么方式改变,都改变的是同一个目标
}
//输出的结果为杰哥你干嘛
静态方法:在静态方法中,可以访问到静态变量,但无法获取成员变量的值
(成员变量是某个具体对象拥有的属性,而静态方法是类具有的,并不是具体对象的,肯定是没办法访问到的。同样的,在静态方法中,无法使用this关键字,因为this关键字代表的是当前的对象本身。)
static void test(){
System.out.println("我是静态方法");
}
静态代码块
static String info;
static { //静态代码块可以用于初始化静态变量
info = "测试";
}
加载类的情况:我们在Java中使用一个类之前,JVM并不会在一开始就去加载它,而是在需要时才会去加载(优化)一般遇到以下情况时才会会加载类:
加载顺序:静态变量>静态代码块>成员变量>普通代码块>构造方法
静态内容在对象构造之前的就完成了初始化,实际上就是类初始化时完成的
public class Person {
String name = test(); //这里我们用test方法的返回值作为变量的初始值,便于观察
int age;
String sex;
{
System.out.println("我是普通代码块");
}
Person(){
System.out.println("我是构造方法");
}
String test(){
System.out.println("我是成员变量初始化"); return "小明";
}
//----------------------------------静态内容---------------------------------------------
static String info = init(); //这里我们用init静态方法的返回值作为变量的初始值,便于观察
static {
System.out.println("我是静态代码块");
}
static String init(){
System.out.println("我是静态变量初始化");
return "test";
}
}
直接访问类的静态变量:可以看到,在使用时确实是先将静态内容初始化之后,才得到值的
public static void main(String[] args) {
System.out.println(Person.info);
}
包其实就是用来区分类位置的东西,也可以用来将我们的类进行分类;
包的命名规则同样是英文和数字的组合,最好是一个域名的格式,比如我们经常访问的www.baidu.com,后面的baidu.com就是域名,我们的包就可以命名为com.baidu,其中的.就是用于分割的,对应多个文件夹;
这里又是一个新的关键字package,这个是用于指定当前类所处的包的,注意,所处的包和对应的目录是一一对应的;
而当我们需要用其他包中的类时,需要先进行导入才可以:这里使用了import关键字导入我们需要使用的类,当然,只有在类不在同一个包下时才需要进行导入,如果一个包中有多个类,我们可以使用表示 * 导入这个包中全部的类:
package com.test; //包名
import com.test.entity.Person; //使用import关键字导入其他包中的类
public class Main {
public static void main(String[] args) {
Person person = new Person(); //只有导入之后才可以使用,否则编译器不知道这个类从哪来的
}
}
注意,在不同包下的类,即使类名相同,也是不同的两个类:那么此时就出现了歧义,编译器不知道到底我们想用的是哪一个String类,所以说我们需要明确指定;
例如:由于默认导入了系统自带的String类,并且也导入了我们自己定义的String类:此时我们只需要在类名前面把完整的包名也给写上,就可以表示这个是哪一个包里的类了,当然,如果没有出现歧义,默认情况下包名是可以省略的,可写可不写。
public class Main {
public static void main(java.lang.String[] args) { //主方法的String参数是java.lang包下的,我们需要明确指定一下,只需要在类名前面添加包名就行了
com.test.entity.String string = new com.test.entity.String();
}
}
Java中引入了访问权限控制(可见性),我们可以为成员变量、成员方法、静态变量、静态方法甚至是类指定访问权限,不同的访问权限,有着不同程度的访问限制:
这四种访问权限,总结如下表:
我们可以提升它的访问权限,来使得外部也可以访问:
public class Person {
public String name; //在name变量前添加public关键字,将其可见性提升为公共等级
int age;
String sex;
}
但是注意,我们创建的普通类不能是protected或是private权限,因为我们目前所使用的普通类要么就是只给当前的包内使用,要么就是给外面都用,如果是private谁都不能用,那这个类定义出来干嘛呢?
如果某个类中存在静态方法或是静态变量,那么我们可以通过静态导入的方式将其中的静态方法或是静态变量直接导入使用,但是同样需要有访问权限的情况下才可以:
public class Person {
String name;
int age;
String sex;
public static void test(){ //定义一个静态方法
System.out.println("我是静态方法!");
}
}
静态导入
import static com.test.entity.Person.test; //静态导入test方法
public class Main {
public static void main(String[] args) {
test(); //直接使用就可以,就像在这个类定义的方法一样
}
}
封装、继承和多态是面向对象编程的三大特性。
封装:把对象的属性和方法结合成一个独立的整体,隐藏实现细节,并提供对外访问的接口。
继承:从已知的一个类中派生出一个新的类,叫子类。子类实现了父类所有非私有化的属性和方法,并根据实际需求扩展出新的行为。
多态:多个不同的对象对同一消息作出响应,同一消息根据不同的对象而采用各种不同的方法。
封装的目的是为了保证变量的安全性,使用者不必在意具体实现细节,而只是通过外部接口即可访问类的成员,如果不进行封装,类中的实例变量可以直接查看和修改,因此在编写类时一般将成员变量私有化,外部类需要使用Getter和Setter方法来查看和设置变量。
封装思想其实就是把实现细节给隐藏了,外部只需知道这个方法是什么作用,而无需关心实现,要用什么由类自己来做,不需要外面来操作类内部的东西去完成,封装就是通过访问权限控制来实现的。
将类的属性变为只能自己直接访问:
public class Person {
private String name; //现在类的属性只能被自己直接访问
private int age;
private String sex;
public Person(String name, int age, String sex) { //构造方法也要声明为公共,否则对象都构造不了
this.name = name;
this.age = age;
this.sex = sex;
}
public String getName() {
return name; //想要知道这个对象的名字,必须通过getName()方法来获取,并且得到的只是名字值,外部无法修改
}
public String getSex() {
return sex;
}
public int getAge() {
return age;
}
}
//主方法
public static void main(String[] args) {
Person person = new Person("小明", 18, "男");
System.out.println(person.getName()); //只能通过调用getName()方法来获取名字 //外部现在只能通过调用我定义的方法来获取成员属性
}
通过setter方法对方法进行额外操作:
public void setName(String name) {
if(name.contains("小")) return;
this.name = name;
}
将构造方法改成私有化,只能通过内部的方式来构造对象:
public class Person {
private String name;
private int age;
private String sex;
private Person(){
} //不允许外部使用new关键字创建对象
public static Person getInstance() { //而是需要使用我们的独特方法来生成对象并返回
return new Person();
}
// public static Person getInstance(){
// 或者构造也在内部进行,无需在外面使用new关键字创建对象
// Person person = new Person();
// person.name = "小明";
// return person;
//}
//主方法
Person person = Person.getInstance();
System.out.println(person.getName());
}
在定义不同类的时候存在一些相同属性,为了方便使用可以将这些共同属性抽象成一个父类,在定义其他子类时可以继承自该父类,减少代码的重复定义,子类可以使用父类中非私有的成员。
想要继承一个类,我们只需要使用extends关键字即可:
具体的实现效果:
public class Person { //先定义一个父类
String name;
int age;
String sex;
}
public class Worker extends Person{ //工人类
}
public class Student extends Person{ //学生类
}
public class Student extends Person{
public void study(){
System.out.println("我的名字是 "+name+",我在学习!"); //可以直接访问父类中定义的name属性
}
}
方法也会被子类继承:
public static void main(String[] args) {
Student student = new Student();
student.study(); //子类不仅有自己的独特技能
student.hello(); //还继承了父类的全部技能
}
//父类
public class Person {
protected String name; //因为子类需要用这些属性,所以说我们就将这些变成protected,外部不允许访问
protected int age;
protected String sex;
//构造方法也改成protected,只能子类用
public Person(String name, int age, String sex) {
this.name = name;
this.age = age;
this.sex = sex;
}
public void hello() {
System.out.println("我叫 " + name + ",今年 " + age + " 岁了!");
}
}
//子类
public class Student extends Person{
//学生类
public Student(String name, int age, String sex) { //因为学生职业已经确定,所以说学生直接填写就可以了
super(name, age, sex); //使用super代表父类,父类的构造方法就是super()
}
public void study(){
System.out.println("我的名字是 "+name+",我在学习!");
}
}
//工人类
public class Worker extends Person{
public Worker(String name, int age, String sex) {
super(name, age, sex, "工人"); //父类构造调用必须在最前面
System.out.println("工人构造成功!"); //注意,在调用父类构造方法之前,不允许执行任何代码,只能在之后执行
}
}
public static void main(String[] args) {
Person person = new Student("小明", 18, "男"); //这里使用父类类型的变量,去引用一个子类对象(向上转型)
person.hello(); //父类对象的引用相当于当做父类来使用,只能访问父类对象的内容
}
public static void main(String[] args) {
Person person = new Student("小明", 18, "男");
Student student = (Student) person; //使用强制类型转换(向下转型)
student.study();
}
public static void main(String[] args) {
Person person = new Student("小明", 18, "男");
if(person instanceof Student) { //我们可以使用instanceof关键字来对类型进行判断
System.out.println("对象是 Student 类型的");
}
if(person instanceof Person) {
System.out.println("对象是 Person 类型的");
}
}
public void study(){
System.out.println("我是 "+super.name+",我在工作!"); //这里使用super.name来表示需要的是父类的name变量
}
public final class Person { //class前面添加final关键字表示这个类已经是最终形态,不能继承
}
实际上所有类都默认继承自Object类,所有类都包含Object类中的方法:
public class Person extends Object{ //除非我们手动指定要继承的类是什么,实际上默认情况下所有的类都是继承自Object的,只是可以省略
}
继承结构为:
既然所有的类都默认继承自Object,我们来看看这个类里面有哪些内容:
public class Object {
private static native void registerNatives(); //标记为native的方法是本地方法,底层是由C++实现的
static {
registerNatives(); //这个类在初始化时会对类中其他本地方法进行注册,本地方法不是我们SE中需要学习的内容,我们会在JVM篇视频教程中进行介绍
} //获取当前的类型Class对象,这个我们会在最后一章的反射中进行讲解,目前暂时不会用到
public final native Class<?> getClass(); //获取对象的哈希值,我们会在第五章集合类中使用到,目前各位小伙伴就暂时理解为会返回对象存放的内存地址
public native int hashCode(); //判断当前对象和给定对象是否相等,默认实现是直接用等号判断,也就是直接判断是否为同一个对象
public boolean equals(Object obj) {
return (this == obj);
} //克隆当前对象,可以将复制一个完全一样的对象出来,包括对象的各个属性
protected native Object clone() throws CloneNotSupportedException; //将当前对象转换为String的形式,默认情况下格式为 完整类名@十六进制哈希值
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
} //唤醒一个等待当前对象锁的线程,有关锁的内容,我们会在第六章多线程部分中讲解,目前暂时不会用到
public final native void notify(); //唤醒所有等待当前对象锁的线程,同上
public final native void notifyAll(); //使得持有当前对象锁的线程进入等待状态,同上
public final native void wait(long timeout) throws InterruptedException; //同上
public final void wait(long timeout, int nanos) throws InterruptedException { ... } //同上
public final void wait() throws InterruptedException { ... } //当对象被判定为已经不再使用的“垃圾”时,在回收之前,会由JVM来调用一次此方法进行资源释放之类的操作,这同样不是SE中需要学习的内容,这个方法我们会在JVM篇视频教程中详细介绍,目前暂时不会用到
protected void finalize() throws Throwable { }
}
方法的重写与重载不同:方法的重载是为某个方法提供更多种类,而方法的重写是覆盖原有的方法实现
注意,静态方法不支持重写,因为它是属于类本身的,但是它可以被继承。
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o; //只有是当前类型的对象,才能进行比较,先转换为当前类型
return this.age == person.age && Objects.equals(this.name, person.name) && //字符串内容的比较,不能使用==,必须使用equals方法
Objects.equals(this.sex, person.sex); //基本类型的比较跟之前一样,直接==
}
public static void main(String[] args) {
Person p1 = new Student("小明", 18, "男");
Person p2 = new Student("小明", 18, "男");
System.out.println(p1.equals(p2));
//此时由于三个属性完全一致,所以说判断结果为真,即使是两个不同的对象
//并且此时的equals为Student中重写的方法
}
多态特性:对于一个类定义的行为,不同的子类可以出现不同的行为,比如在父类中定义一个考试的方法,在子类中重写该方法得到不同的行为
//父类
public class Person {
...
public void exam(){
System.out.println("我是考试方法");
}
...
}
public class Student extends Person{
...
@Override
public void exam() {
System.out.println("我是学生,我就是小镇做题家,拿个 A 轻轻松松");
}
}
public class Worker extends Person{
...
@Override
public void exam() {
System.out.println("我是工人,做题我并不擅长,只能得到 D");
}
}
//主方法
public static void main(String[] args) {
Person person = new Student("小明", 18, "男");
person.exam();
person = new Worker("小强", 18, "男");
person.exam();
}
无法让子类重写方法的操作:
在重写父类方法时,需要注意:
@Override
public void exam() {
super.exam(); //调用父类的实现
System.out.println("我是工人,做题我并不擅长,只能得到 D");
}
protected void exam(){ //父类中的方法权限为protected
System.out.println("我是考试方法");
}
public void exam() { //子类中将可见性提升为public
System.out.println("我是工人,做题我并不擅长,只能得到 D");
}
抽象类父类只定义一个抽象概念,具体实现由子类来实现,需要通过关键字abstract来实现
public abstract class Person { //通过添加abstract关键字,表示这个类是一个抽象类
protected String name; //大体内容其实普通类差不多
protected int age;
protected String sex;
protected String profession;
protected Person(String name, int age, String sex, String profession) {
this.name = name;
this.age = age;
this.sex = sex;
this.profession = profession;
}
public abstract void exam(); //抽象类中可以具有抽象方法,也就是说这个方法只有定义,没有方法体
}
注意:
public class Worker extends Person{
public Worker(String name, int age, String sex) {
super(name, age, sex, "工人");
}
@Override
public void exam() { //子类必须要实现抽象类所有的抽象方法,这是强制要求的,否则会无法通过编译
System.out.println("我是工人,做题我并不擅长,只能得到 D");
}
}
public abstract class Student extends Person{ //如果抽象类的子类也是抽象类,那么可以不用实现父类中的抽象方法
public Student(String name, int age, String sex) {
super(name, age, sex, "学生");
}
@Override //抽象类中并不是只能有抽象方法,抽象类中也可以有正常方法的实现
public void exam() {
System.out.println("我是学生,我就是小镇做题家,拿个 A 轻轻松松");
}
}
接口就是只包含方法的定义,甚至都不是一个类!接口一般只代表某些功能的抽象,接口包含了一些列方法的定义,类可以实现这个接口,表示类支持接口代表的功能(类似于一个插件,只能作为一个附属功能加在主体上,同时具体实现还需要由主体来实现)
实际上接口的目标就是将类所具有某些的行为抽象出来。
有些人说接口其实就是Java中的多继承,但是我个人认为这种说法是错的,实际上实现接口更像是一个类的功能列表,作为附加功能存在,一个类可以附加很多个功能,接口的使用和继承的概念有一定的出入,顶多说是多继承的一种替代方案
public interface Study { //使用interface表示这是一个接口
void study(); //接口中只能定义访问权限为public抽象方法,其中public和abstract关键字可以省略
}
用类来实现这个接口:
public class Student extends Person implements Study { //使用implements关键字来实现接口
public Student(String name, int age, String sex) {
super(name, age, sex);
}
@Override
public void study() { //实现接口时,同样需要将接口中所有的抽象方法全部实现
System.out.println("我会学习!");
}
}
接口不同于继承,接口可以同时实现多个:
public class Student extends Person implements Study, A, B, C { //多个接口的实现使用逗号隔开
}
接口跟抽象类一样,不能直接创建对象,但是我们也可以将接口实现类的对象以接口的形式去使用,并且当做接口使用时,只有接口中定义的方法和Object类的方法,无法使用类本身的方法和父类的方法。
Study study = new Student("小王",18,"男"); //类的对象(Studennt)以接口的类型(Study)来使用
study.study(); //只有接口中的方法
接口的向下转型:
public static void main(String[] args) {
Study study = new Teacher("小王", 27, "男");
Teacher teacher = (Teacher) study; //强制类型转换
teacher.study();
}
接口中存在方法的默认实现:使用default关键字来默认实现,并且在实现类中就不强制要求重写该方法了,如果非要重写方法,通过接口名.super.方法()来实现
//接口
public interface Study {
void study();
default void test() { //使用default关键字为接口中的方法添加默认实现
System.out.println("我是默认实现");
}
}
//实现类
public class Student extends Person implements Study{
@Override
public void test() {
Study.super.test();
}
}
接口不同于类,接口中不允许存在成员变量和成员方法,但是可以存在静态变量和静态方法,在接口中定义的变量只能是:
public interface Study {
public static final int a = 10; //接口中定义的静态变量只能是public static final的
public static void test(){ //接口中定义的静态方法也只能是public的
System.out.println("我是静态方法");
}
void study();
}
跟普通的类一样,我们可以直接通过接口名 . 的方式使用静态内容:
public static void main(String[] args) {
System.out.println(Study.a); //无法进行修改
Study.test();
}
最后介绍一下Object类中的Clone克隆方法,它需要实现接口才可以使用,实现接口后,我们还需要将克隆方法的可见性提升一下,不然还用不了
package java.lang;
public interface Cloneable { //这个接口中什么都没定义
}
实现接口后,我们还需要将克隆方法的可见性提升一下,不然还用不了,接口中默认是protected 需要改成public
public class Student extends Person implements Study, Cloneable { //首先实现Cloneable接口,表示这个类具有克隆的功能
public Student(String name, int age, String sex) {
super(name, age, sex, "学生");
}
@Override
public Object clone() throws CloneNotSupportedException { //提升clone方法的访问权限
return super.clone(); //因为底层是C++实现,我们直接调用父类的实现就可以了
}
}
接着我们来尝试一下,看看是不是会得到一个一模一样的对象:
public static void main(String[] args) throws CloneNotSupportedException { //这里向上抛出一下异常,还没学异常,所以说照着写就行了
Student student = new Student("小明", 18, "男");
Student clone = (Student) student.clone(); //调用clone方法,得到一个克隆的对象
System.out.println(student);
System.out.println(clone);
System.out.println(student == clone);
System.out.println(student.equals(clone));
}
可以发现,原对象和克隆对象,是两个不同的对象,但是他们的各种属性都是完全一样的:
克隆操作:可以完全复制一个对象的所有属性
但是像这样的拷贝操作其实也分为浅拷贝和深拷贝:
对于克隆来说,虽然Student对象成功拷贝,但是其内层对象并没有进行拷贝,依然只是对象引用的复制,所以Java为我们提供的clone方法只会进行浅拷贝。
接口是可以继承其他接口的,并且可以多继承:
public interface sleep extends Study,A,B{
}
如果子接口继承父类时,子接口中存在与父接口同名的方法,则子接口的方法覆盖父接口的方法:
当接口中已经实现了该方法了,在实现类中就不需要再去实现了,比如equals方法,默认在Object中实现了该方法
//接口
public interface Study extends sleep{
boolean equals(Object obj);
}
并且equals无法重写Object中的成员
当子类同时继承父类与接口,并且存在相同的方法,首先会实现的是父类中的方法,之后才是接口中的方法:
public class Test2 extends Test implements A{
}
能够更好地去实现这样的状态标记,我们希望开发者拿到使用的就是我们预先定义好的状态,所以,我们可以使用枚举类来完成
枚举类型使用起来就非常方便了,其实枚举类型的本质就是一个普通的类,但是它继承自Enum类,我们定义的每一个状态其实就是一个public static final的Status类型成员变量:
枚举类:
public enum Status { //enum表示这是一个枚举类,枚举类的语法稍微有一些不一样
RUNNING, STUDY, SLEEP; //直接写每个状态的名字即可,最后面分号可以不打,但是推荐打上
}
使用枚举类也非常方便,就像使用普通类型那样
public class Student{
private Status status; //类型变成刚刚定义的枚举类
public Status getStatus() {
return status;
}
public void setStatus(Status status) {
this.status = status;
}
}
这样,别人在使用时,就能很清楚地知道我们支持哪些了:
![[Pasted image 20230508215211.png]]
public enum Status {
RUNNING("睡觉"), STUDY("学习"), SLEEP("睡觉"); //无参构造方法被覆盖,创建枚举需要添加参数(本质就是调用的构造方法)
private final String name; //枚举的成员变量
Status(String name){ //覆盖原有构造方法(默认private,只能内部使用!)
this.name = name;
}
public String getName() { //获取封装的成员变量
return name;
}
}
这样,枚举就可以按照我们想要的中文名称打印了:
public static void main(String[] args) {
Student student = new Student("小明", 18, "男");
student.setStatus(Status.RUNNING);
System.out.println(student.getStatus().getName()); }
Java中的基本类型,如果想通过对象的形式去使用他们,Java提供的基本类型包装类,使得Java能够更好的体现面向对象的思想,同时也使得基本类型能够支持对象操作!
其中能够表示数字的基本类型包装类,继承自Number类,对应关系如下表:
public static void main(String[] args) {
Integer i = new Integer(10); //将10包装为一个Integer类型的变量
}
包装类型支持自动装箱,我们可以直接将一个对应的基本类型值作为对应包装类型引用变量的值,这里本质上就是被自动包装成了一个Integer类型的对象:
public static void main(String[] args) {
Integer i = 10; //将int类型值作为包装类型使用
}
既然能装箱,也是支持拆箱的,自动拆箱,将i用intValue:
public static void main(String[] args) {
Integer i = 10;
int a = i.intValue(); //通过此方法变成基本类型int值
}
两种特殊的包装类型: 第一个是用于计算超大数字的BigInteger:
public static void main(String[] args) {
BigInteger i = BigInteger.valueOf(Long.MAX_VALUE); //表示Long的最大值,轻轻松松
System.out.println(i);
}
调用类中的方法,进行运算操作:
public static void main(String[] args) {
BigInteger i = BigInteger.valueOf(Long.MAX_VALUE);
i = i.multiply(BigInteger.valueOf(Long.MAX_VALUE)); //即使是long的最大值乘以long的最大值,也能给你算出来
System.out.println(i);
}
第二个是用于计算小数的精确计算的BigDecimal:
public static void main(String[] args) {
BigDecimal i = BigDecimal.valueOf(10);
i = i.divide(BigDecimal.valueOf(3), 100, RoundingMode.CEILING);
//计算10/3的结果,精确到小数点后100位
//RoundingMode是舍入模式,就是精确到最后一位时,该怎么处理,这里CEILING表示向上取整,
//FLOOR表示向下取整
System.out.println(i);
}
定义一个数组变量,并且注意数组类型本身也是类,也是以对象的形式存在的,并不是基本数据类型,所以创建一个数组,同样需要使用new关键字:
public static void main(String[] args) {
int[] array; //类型[]就表示这个是一个数组类型
}
初始化:第一种初始化方式:
public static void main(String[] args) {
int[] array1 = new int[10]; //在创建数组时,需要指定数组长度,也就是可以容纳多个int变量的值
Object obj = array; //因为同样是类,肯定是继承自Object的,所以说可以直接向上转型 int→object
}
第二种:
int array2[] = new int[10];//支持C语言样式,但不推荐!
第三种:
int[] array3 = new int[]{1,2,3,4,5};//静态初始化(直接指定值和大小)
第四种:
int[] array4 = {1,2,3,4,5};//同上,但是只能在定义时赋值
注意:数组的下标是从0开始的,不是从1开始的,并且注意千万别写成负数或是超出范围了,否则会出现异常。
既然是类型,那么肯定也是继承object类的:但是object类的方法没有重写,也就是说依然采用object中的默认实现:
public static void main(String[] args) {
int[] array = new int[10];
System.out.println(array.toString());
System.out.println(array.equals(array));
}
遍历数组的两种方法:第一种基本方法:
public static void main(String[] args) {
int[] array = new int[10];
for (int i = 0; i < array.length; i++) { //最基本的方法
System.out.print(array[i] + " ");
}
}
第二种简化版的for语句foreach语法来遍历数组中的每一个元素;
也可以通过快捷方式array.for直接快速生成;
public static void main(String[] args) {
int[] array = new int[10];
for (int i : array) { //int i就是每一个数组中的元素,array就是我们要遍历的数组
System.out.print(i+" "); //每一轮循环,i都会更新成数组中下一个元素
}
}
关于基本数据类型和引用类型:
public static void main(String[] args) {
int[] arr = new int[10];
Integer[] test = arr; //这里是无法实现的,因为基本类型int无法装箱得到基本类型包装类
}
但是如果是引用类型的话,是可以的:
public static void main(String[] args) {
String[] arr = new String[10];
Object[] array = arr; //数组同样支持向上转型
}
public static void main(String[] args) {
Object[] arr = new String[10]; //这里必须是new一个String类型,不然无法强转
String[] array = (String[]) arr; //也支持向下转型
}
final int[] array3 = new int[]{1,2,3,4,5};
array3[1] = 3; //可以改变他的值
System.out.println(array3[1]);
但是没法赋值到新的对象:
定义一个二维数组:第一种
public static void main(String[] args) {
int[][] array = new int[2][10]; //数组类型数组那么就要写两个[]了
}
第二种
public static void main(String[] args) {
int[][] arr = { {1, 2},
{3, 4},
{5, 6} }; //一个三行两列的数组
System.out.println(arr[2][1]); //访问第三行第二列的元素
}
第三种:相当于第一第二种结合
public static void main(String[] args) {
int[][] arr = new int[][]{ {1, 2}, {3, 4}, {5, 6} };
}
遍历多维数组:
public static void main(String[] args) {
int[][] twoarrary = new int[3][4];
for (int i = 0; i < twoarrary.length; i++) { //i对应的是一共有几行
for (int j = 0; j < twoarrary[i].length; j++) { //j对应的是第i行有多少个元素
System.out.print(twoarrary[i][j]);
}
System.out.println();
}
}
实际上我们的方法是支持可变长参数的,使用时,可以传入0-N个对应类型的实参:
public class Main {
public static void main(String[] args) {
test("1","2","3");
}
public static void test(String... str){ // string类型的可变长参数,可以传入0-N个对应类型的实参
for (String s : str) { //遍历数组
System.out.println(s);
}
}
}
注意:
public void test(int a, int b, String... strings){ //必须放在最后才行
}
public static void main(String[] args) {
//实际上这个是我们在执行Java程序时,输入的命令行参数
}
在终端上添加内容:
java com/test/Main lbwnb aaaa xxxxx #放在包中需要携带主类完整路径才能运行
可以看到,我们在后面随意添加的三个参数,都放到数组中了:
首先String本身也是一个类,只不过它比较特殊,每个用双引号(“”)括起来的字符串,都是String类型的一个实例对象:
public static void main(String[] args) {
String str = "Hello World!";
}
注意,如果是直接使用双引号创建的字符串,如果内容相同,为了优化效率,那么始终都是同一个对象:
public static void main(String[] args) {
String str1 = "Hello World";
String str2 = "Hello World";
System.out.println(str1 == str2); //两者属于同一个对象
}
如果使用构造方法主动创建两个新的对象,就是不同的对象了:
public static void main(String[] args) {
String str1 = new String("Hello World");
String str2 = new String("Hello World");
System.out.println(str1 == str2); //两者属于不同的对象
}
因此,如果我们仅仅是想要判断两个字符串的内容是否相同,不要使用 == 他只是比较两者的对象是否相同,String类重载了equals方法用于判断和比较内容是否相同:
public static void main(String[] args) {
String str1 = new String("Hello World");
String str2 = new String("Hello World");
System.out.println(str1.equals(str2)); //字符串的内容比较,一定要用equals
}
字符串中提供了很多方便操作的方法,比如字符串的裁剪、分割操作:
public static void main(String[] args) {
String str = "Hello World";
String sub = str.substring(0, 3); //分割字符串,并返回一个新的子串对象
System.out.println(sub);
}
split()方法:分割字符串
public static void main(String[] args) {
String str = "Hello World";
String[] strings = str.split(" "); //使用split方法进行字符串分割,比如这里就是通过空格分隔,得到一个字符串数组
for (String string : strings) {
System.out.println(string);
}
}
字符数组和字符串之间是可以快速进行相互转换的
public static void main(String[] args) {
String str1 ="hello world!";
char[] chars = str1.toCharArray(); //由字符串转移到字符数组
for (char aChar : chars) {
System.out.print(aChar);
}
}
由字符数组转移到字符串:
public static void main(String[] args) {
char[] chars = new char[]{'奥', '利', '给'};
String str = new String(chars); //由字符数组转移到字符串
System.out.println(str);
}
字符串可以使用 + 来进行拼接操作,对于变量来说,直接使用加的话,每次运算都会生成一个新的对象
public static void main(String[] args) {
String str1 = "你看";
String str2 = "这";
String str3 = "汉堡";
String str4 = "做滴";
String str5 = "行不行";
String result = str1 + str2 + str3 + str4 + str5; //5个变量连续加,其中生成了4个字符串对象出来
System.out.println(result);
}
对这种情况进行优化:
public static void main(String[] args) {
String str1 = "你看";
String str2 = "这";
String str3 = "汉堡";
String str4 = "做滴";
String str5 = "行不行";
StringBuilder builder = new StringBuilder(); //创建StringBuilder的类型
builder.append(str1).append(str2).append(str3).append(str4).append(str5); //进行append拼接
System.out.println(builder.toString());
}
public static void main(String[] args) {
StringBuilder builder = new StringBuilder(); //一开始创建时,内部什么都没有
builder.append("AAA"); //我们可以使用append方法来讲字符串拼接到后面
builder.append("BBB");
System.out.println(builder.toString()); //当我们字符串编辑完成之后,就可以使用toString转换为字符串了
}
public static void main(String[] args) {
StringBuilder bulider = new StringBuilder();
bulider.append("AAA");
bulider.append("BBB");
bulider.replace(0,3,"CCC"); //start end 需要替换成的字符
System.out.println(bulider);
}
}
public static void main(String[] args) {
StringBuilder bulider = new StringBuilder();
bulider.append("AAA");
bulider.append("BBB");
bulider.reverse(); //反转
System.out.println(bulider);
}
}
public static void main(String[] args) {
StringBuilder bulider = new StringBuilder();
bulider.append("AAA")
.append("BBB")
.reverse(); //链式调用
System.out.println(bulider);
}
我们现在想要实现这样一个功能,对于给定的字符串进行判断,如果字符串符合我们的规则,那么就返回真,否则返回假
正则表达式(regular expression) 描述了一种字符串匹配的模式(pattern),可以用来检查一个串是否含有某种子串、将匹配的子串替换或者从某个串中取出符合某个条件的子串等
一般通过matches方法用于对给定正则表达式进行匹配,匹配成功返回true,否则返回false
public static void main(String[] args) {
String str = "oooo";
//matches方法用于对给定正则表达式进行匹配,匹配成功返回true,否则返回false
System.out.println(str.matches("o+")); //+表示对前面这个字符匹配一次或多次,这里字符串是oooo,正好可以匹配
}
用于规定给定组件必须要出现多少次才能满足匹配的,我们一般称为限定符,限定符表如下:
如果我们想要表示一个范围内的字符,可以使用方括号:
public static void main(String[] args) {
String str = "abcabccaa";
System.out.println(str.matches("[abc]*")); //表示abc这几个字符可以出现 0 - N 次
}
对于普通字符来说,我们可以下面的方式实现多种字符匹配:
当然,这里仅仅是对正则表达式的简单使用,实际上正则表达式内容非常多,如果需要完整学习正则表达式,可以到:https://www.runoob.com/regexp/regexp-syntax.html
正则表达式并不是只有Java才支持,其他很多语言比如JavaScript、Python等等都是支持正则表达式的。
内部类顾名思义,就是创建在内部的类
我们可以直接在类的内部定义成员内部类:
public class Test {
public class Inner { //内部类也是类,所以说里面也可以有成员变量、方法等,甚至还可以继续套娃一个成员内部类
public void test(){
System.out.println("我是成员内部类!");
}
}
}
public static void main(String[] args) {
Test test = new Test(); //我们首先需要创建对象
Test.Inner inner = test.new Inner(); //成员内部类的类型名称就是 外层.内部类名称
}
public static void main(String[] args) {
Test test = new Test();
Test.Inner inner = test.new Inner();
inner.test(); //使用内部类的方法
}
public class Test {
private final String name;
public Test(String name){
this.name = name;
}
public class Inner {
public void test(){
System.out.println("我是成员内部类:"+name);
//成员内部类可以访问到外部的成员变量
//因为成员内部类本身就是某个对象所有的,每个对象都有这样的一个类定义,这里的name是其所依附对象的
}
}
}
具体解释:每个类可以创建一个对象,每个对象中都有一个单独的类定义,可以通过这个成员内部类又创建出更多对象,套娃行为
反之,外部如何访问到内部类的成员变量:
具体是:如果内部类中定义了一个同名的变量,我们怎么区分使用的是哪一个;
public class Test {
private final String name;
public Test(String name){
this.name = name;
}
public class Inner {
String name;
public void test(String name){
System.out.println("方法参数的name = "+name); //依然是就近原则,最近的是参数,那就是参数了
System.out.println("成员内部类的name = "+this.name); //在内部类中使用this关键字,只能表示内部类对象
System.out.println("成员内部类的name = "+Test.this.name); //如果需要指定为外部的对象,那么需要在前面添加外部类型名称
}
}
}
扩展到对方法的调用和super关键字的使用也是一样:
public class Test {
public class Inner {
String name;
public void test(String name){
this.toString(); //内部类自己的toString方法
super.toString(); //内部类父类的toString方法
Test.this.toString(); //外部类的toSrting方法
Test.super.toString(); //外部类父类的toString方法
}
}
}
对于静态内部类,我们可以直接创建使用,不需要依附于任何对象,可以直接创建静态内部类的对象:
public class Test {
private final String name;
public Test(String name){
this.name = name;
}
public static class Inner {
public void test(){
System.out.println("我是静态内部类!");
}
}
}
public static void main(String[] args) {
Test.Inner inner = new Test.Inner(); //静态内部类的类名同样是之前的格式,但是可以直接new了
inner.test();
}
静态内部类由于是静态的,整个内部类都处于静态是无法访问到外部类的非静态内容
这是因为静态内部类是属于外部类,不依附任何对象,那么我直接访问外部类的非静态属性,具体是访问哪个对象就不知道了
局部内部类就像局部变量一样,可以在方法中定义。
public class Test {
private final String name;
public Test(String name){
this.name = name;
}
public void hello(){
class Inner { //直接在方法中创建局部内部类
}
}
}
既然是在方法中声明的类,那作用范围也就只能在方法中了:
public class Test {
public void hello(){
class Inner{ //局部内部类跟局部变量一样,先声明后使用
public void test(){
System.out.println("我是局部内部类");
}
}
Inner inner = new Inner(); //局部内部类直接使用类名就行, //只作用于方法中
inner.test();
}
}
抽象类和接口中存在的抽象方法需要子类去实现,但不能直接通过new的方式去创建一个抽象类或接口对象,但是我们可以使用匿名内部类
public abstract class Student { //抽象类
public abstract void test();
}
正常情况下,要创建一个抽象类的实例对象,只能对其进行继承,先实现未实现的方法,然后创建子类对象
而现在我们可以直接在方法中使用匿名内部类,将其中的抽象方法实现,并直接创建实例对象:
所谓的匿名内部类:这里创建出来的Student对象,就是一个已经实现了抽象方法的对象,这个抽象类直接就定义好了,甚至连名字都没有,就可以直接就创出对象。
//在主方法中实现
public static void main(String[] args) {
Student student = new Student() { //在new的时候,后面加上花括号,把未实现的方法实现了
@Override
public void test() {
System.out.println("我是匿名内部类的实现!");
}
};
student.test(); //直接定义,连名字都不需要。就可以直接创建出对象
}
public static void main(String[] args) {
Study study = new Study() { //Study表示的是一个接口类型
@Override
public void study() {
System.out.println("我是学习方法!");
}
};
study.study();
}
匿名内部类中同样可以使用类中的属性(因为它本质上就相当于是对应类型的子类) 所以说:
public static void main(String[] args) {
Student student = new Student() {
{
test(); //执行代码块
}
@Override
public void test() {
name = "匿名内部类初始化的变量";
System.out.println("方法执行");
}
};
System.out.println(student.getName());
}
前面通过匿名内部类,创建一个临时的实现子类,此时如果一个接口中有且只有一个待实现的抽象方法,那么我们可以将匿名内部类简写为Lambda表达式:
public static void main(String[] args) {
Study study = () -> System.out.println("我是学习方法!"); //是不是感觉非常简洁!
study.study();
}
关于Lambda表达式的具体规范:
() -> System.out.println("我是学习方法!"); //跟之前流程控制一样,如果只有一行代码花括号可省略
public static void main(String[] args) {
sleep sleep = (a,b) -> {
System.out.println("正在睡觉");
System.out.println("睡醒了");
System.out.println("直接睡"); //实际上这里面就是方法体,该咋写咋写
return "今天学会了"+a;
};
System.out.println(sleep.sleep(1,2));
}
Study study = (a) -> "今天学会了"+a; //简化版
Study study = a -> "今天学会了"+a;
如果一个方法的参数需要一个接口的实现:
public static void main(String[] args) {
test(a -> "今天学会了"+a); //参数直接写成lambda表达式
}
private static void test(Study study){
System.out.println(sleep.sleep(10));
}
方法引用就是将一个已实现的方法,直接作为接口中抽象方法的实现(当然前提是方法定义得一样才行)
方法引用其实本质上就相当于将其他方法的实现,直接作为接口中抽象方法的实现。任何方法都可以通过方法引用作为实现:
简单的例子:求和方法
public interface Study {
int sum(int a, int b); //待实现的求和方法
}
此时,我们可以直接将已有方法的实现作为接口的实现 :直接使用方法引用 ::双冒号来进行方法引用 静态方法使用 类名::方法名 的形式
Integer类中默认提供两数之和的方法,直接拿来作为接口中求和方法的实现
public static void main(String[] args) {
Study study = Integer::sum; //使用双冒号来进行方法引用,静态方法使用 类名::方法名 的形式
System.out.println(study.sum(10, 20));
}
public static void main(String[] args) {
Main main = new Main(); //对于成员方法要new一个对象
Study study = main::lbwnb; //成员方法因为需要具体对象使用,所以说只能使用 对象::方法名 的形式
}
public String lbwnb(){ //该方法为成员方法
return "卡布奇诺今犹在,不见当年倒茶人。";
}
public static void main(String[] args) {
Study study = String::new; //没错,构造方法也可以被引用,使用new表示
}
异常机制:我们在之前其实已经接触过一些异常了,比如数组越界异常(ArrayIndexOutOfBoundsException),空指针异常(NullPointerException),算术异常(ArithmeticException) 等,他们其实都是异常类型,我们的每一个异常也是一个类,他们都继承自Exception类!异常类型本质依然类的对象,但是异常类型支持在程序运行出现问题时抛出(也就是上面出现的红色报错);也可以提前声明,告知使用者需要处理可能会出现的异常!
第一种类型是运行时异常,如上述的列子,在编译阶段无法感知代码是否会出现问题,只有在运行的时候才知道会不会出错(正常情况下是不会出错的),这样的异常称为运行时异常,异常也是由类定义的,所有的运行时异常都继承自RuntimeException。
另一种类型是编译时异常,编译时异常明确指出可能会出现的异常,在编译阶段就需要进行处理(捕获异常)必须要考虑到出现异常的情况,如果不进行处理,将无法通过编译!默认继承自Exception类的异常都是编译时异常。
如Object类中定义的clone方法,就明确指出了在运行的时候会出现的异常
protected native Object clone() throws CloneNotSupportedException; //编译时异常
还有一种类型是错误(error),错误比异常更严重,异常就是不同寻常,但不一定会导致致命的问题,而错误是致命问题,一般出现错误可能JVM就无法继续正常运行了,比如OutOfMemoryError就是内存溢出错误(内存占用已经超出限制,无法继续申请内存了)
当别人调用我们的方法时,如果传入了错误的参数导致程序无法正常运行,这时我们就可以手动抛出一个异常来终止程序通过throw关键字继续运行下去,同时告知上一级方法执行出现了问题:
运行时异常(RuntimeException):异常的抛出同样需要创建一个异常对象出来,我们抛出异常实际上就是将这个异常对象抛出,异常对象携带了我们抛出异常时的一些信息,比如是因为什么原因导致的异常,在RuntimeException的构造方法中我们可以写入原因。
public static int test(int a, int b) {
if(b == 0)
throw new RuntimeException("被除数不能为0"); //使用throw关键字来抛出异常
return a / b;
}
当出现于异常时:程序会终止,并且会打印栈追踪信息,我们这里就简单介绍一下,实际上方法之间的调用是有层级关系的,而当异常发生时,方法调用的每一层都会在栈追踪信息中打印出来,比如这里有两个at,实际上就是在告诉我们程序运行到哪个位置时出现的异常,位于最上面的就是发生异常的最核心位置,我们代码的第15行
编译时异常(Exception):必须告知函数的调用方我们会抛出某个异常,函数调用方必须要对抛出的这个异常进行对应的处理才可以:
private static void test() throws Exception { //使用throws关键字告知调用方此方法会抛出哪些异常,请调用方处理好
throw new Exception("我是编译时异常!");
}
注意,如果不同的分支条件会出现不同的异常,那么所有在方法中可能会抛出的异常都需要注明:
private static void test(int a) throws FileNotFoundException, ClassNotFoundException { //多个异常使用逗号隔开
if(a == 1)
throw new FileNotFoundException();
else
throw new ClassNotFoundException();
}
补充:当然,并不是只有非运行时异常可以像这样明确指出,运行时异常也可以,只不过不强制要求;
最后再提一下,我们在重写方法时,如果父类中的方法表明了会抛出某个异常,只要重写的内容中不会抛出对应的异常我们可以直接省去;
为了让程序继续运行下去,就需要对异常进行捕获:通过使用tyr-catch语句进行异常捕获:
public static void main(String[] args) {
try { //使用try-catch语句进行异常捕获
test(1, 0); //可能会发生异常的代码
} catch (ArithmeticException e) { //因为异常本身也是一个对象,catch中实际上就是用一个局部变量去接收异常
e.printStackTrace(); //可以在catch语句中对捕获到的异常进行处理;打印栈追踪信息
System.out.println("异常错误信息:"+e.getMessage()); //获取异常的错误信息
}
System.out.println("程序继续正常运行");
}
private static int test(int a,int b){
if (b == 0){
throw new ArithmeticException("除数不能为0");
}
return a / b;
}
此时,当我们捕获异常之后,程序可以继续正常运行,并不会像之前一样直接结束掉。
注意,catch中捕获的类型只能是Throwable的子类,也就是说要么是抛出的异常,要么是错误,不能是其他的任何类型。
private static int test(int a,int b){
if (b == 0){
try {
throw new Exception("除数不能为0"); //为编译时异常,必须使用try-catch来进行异常捕获
} catch (Exception e) {
throw new RuntimeException(e);
}
}
return a / b;
}
或者不在当前的方法中处理,直接抛给上一级:
private static int test(int a,int b) throws Exception { //直接抛给上一级处理
if (b == 0){
throw new Exception("除数不能为0");
}
return a / b;
}
public static void main(String[] args) {
try {
int[] arr = new int[10];
arr[-1] = 10;
}catch (NullPointerException e){
}catch (ArrayIndexOutOfBoundsException e ){ //优先匹配前面的
System.out.println("数组越界异常");
}catch (RuntimeException e){ //后面的异常如果也可以被匹配,但不会被匹配
System.out.println("运行时异常");
}
}
try {
//....
} catch (RuntimeException e){ //父类型在前,会将子类的也捕获
} catch (NullPointerException e) { //子类永远都不会被捕获
} catch (IndexOutOfBoundsException e){ //子类永远都不会被捕获
}
try {
//....
} catch (NullPointerException | IndexOutOfBoundsException e) { //用|隔开每种类型即可
}
最后,当我们希望,程序运行时,无论是否出现异常,都会在最后执行任务,可以交给finally语句块来处理:
try {
//....
}catch (Exception e){
}finally {
System.out.println("lbwnb"); //无论是否出现异常,都会在最后执行
}
我们可以使用断言表达式来对某些东西进行判断,如果判断失败会抛出错误,只不过默认情况下没有开启断言,我们需要在虚拟机参数中:
断言表达式需要使用到assert关键字,如果assert后面的表达式判断结果为false,将抛出AssertionError错误。
public static void main(String[] args) {
assert false;
}
比如我们可以判断变量的值,如果大于10就抛出错误,也可以在表达式的后面添加错误信息:
public static void main(String[] args) {
int a = 10;
assert a > 10 : "我是自定义的错误信息";
}
断言表达式一般只用于测试,我们正常的程序中一般不会使用,这里只做了解就行了。
工具类就是专门为一些特定场景编写的,便于我们去使用的类,工具类一般都会内置大量的静态方法,我们可以通过类名直接使用。
public static void main(String[] args) {
//Math也是java.lang包下的类,所以说默认就可以直接使用
System.out.println(Math.pow(5, 3)); //我们可以使用pow方法直接计算a的b次方
Math.abs(-1); //abs方法可以求绝对值
Math.max(19, 20); //快速取最大值
Math.min(2, 4); //快速取最小值
Math.sqrt(9); //求一个数的算术平方根
}
Math.sin(Math.PI / 2); //求π/2的正弦值,这里我们可以使用预置的PI进行计算
Math.cos(Math.PI); //求π的余弦值
Math.tan(Math.PI / 4); //求π/4的正切值
Math.asin(1); //三角函数的反函数也是有的,这里是求arcsin1的值
Math.acos(1);
Math.atan(0);
1.2246467991473532×10−16 这其实是科学计数法的10,后面的数就是指数:其实这个数是非常接近于0,这是因为精度问题导致的,所以说实际上结果就是0。
我们也可以快速计算对数函数:
public static void main(String[] args) {
Math.log(Math.E); //e为底的对数函数,其实就是ln,我们可以直接使用Math中定义好的e
Math.log10(100); //10为底的对数函数
//利用换底公式,我们可以弄出来任何我们想求的对数函数
double a = Math.log(4) / Math.log(2); //这里是求以2为底4的对数,log(2)4 = ln4 / ln2
System.out.println(a);
}
public static void main(String[] args) {
Math.ceil(4.5); //通过使用ceil来向上取整 取值为5
Math.floor(5.6); //通过使用floor来向下取整 取值为5
}
这里我们再介绍一下随机数的生成,Java中想要生成一个随机数其实也很简单,我们需要使用Random类来生成(这个类时java.util包下的,需要手动导入才可以)
public static void main(String[] args) {
Random random = new Random(); //创建Random对象
for (int i = 0; i < 30; i++) {
System.out.print(random.nextInt(100)+" "); //nextInt方法可以指定创建0 - x之内的随机数
}
}
那么有没有一个比较方便的方式去使用数组呢?我们可以使用数组工具类Arrays。
这个类也是java.util包下类,它用于便捷操作数组
public static void main(String[] args) {
int[] arr = new int[]{1, 4, 5, 8, 2, 0, 9, 7, 3, 6};
System.out.println(Arrays.toString(arr));
}
public static void main(String[] args) {
int[] arr = new int[]{1, 4, 5, 8, 2, 0, 9, 7, 3, 6};
Arrays.sort(arr); //可以对数组进行排序,将所有的元素按照从小到大的顺序排放
System.out.println(Arrays.toString(arr));
}
public static void main(String[] args) {
int[] arr = new int[10];
Arrays.fill(arr, 66);
System.out.println(Arrays.toString(arr));
}
public static void main(String[] args) {
int[] arr = new int[]{1, 2, 3, 4, 5};
int[] target = Arrays.copyOf(arr, 5); //arr的全部范围
System.out.println(Arrays.toString(target)); //拷贝数组的全部内容,并生成一个新的数组对象
System.out.println(arr == target);
}
public static void main(String[] args) {
int[] arr = new int[]{1, 2, 3, 4, 5};
int[] target = Arrays.copyOfRange(arr, 3, 5); //也可以只拷贝某个范围内的内容,前含后不含
System.out.println(Arrays.toString(target));
System.out.println(arr == target);
}
public static void main(String[] args) {
int[] arr = new int[]{1, 2, 3, 4, 5};
int[] target = new int[10];
System.arraycopy(arr, 0, target, 0, 5); //使用System.arraycopy进行搬运
//第一个0为arr从哪个位置开始取,第二个0为存到target的哪个位置,第三个5为取出的arr的长度
System.out.println(Arrays.toString(target));
}
public static void main(String[] args) {
int[] arr = new int[]{1, 2, 3, 4, 5};
System.out.println(Arrays.binarySearch(arr, 5)); //二分搜索仅适用于有序数组 第二个数字为要查询的数字 }
public static void main(String[] args) {
int[][] array = new int[][]{{2, 8, 4, 1}, {9, 2, 0, 3}};
System.out.println(Arrays.deepToString(array)); //deepToString方法可以对多维数组进行打印
}
同样的,因为数组本身没有重写equals方法,所以说无法判断两个不同的数组对象中的每一个元素是否相同,Arrays也为一维数组和多维数组提供了相等判断的方法:
public static void main(String[] args) {
int[][] a = new int[][]{{2, 8, 4, 1}, {9, 2, 0, 3}};
int[][] b = new int[][]{{2, 8, 4, 1}, {9, 2, 0, 3}};
System.out.println(Arrays.equals(a, b)); //equals仅适用于一维数组
System.out.println(Arrays.deepEquals(a, b)); //对于多维数组,需要使用deepEquals来进行深层次判断
}
注意的是:equals传入的是基本数据类型,deepEquals传入的是对象数据类型,所以一维数组只能使用equals方法进行比较,而二维数组可以使用deepEquals方法,是因为二维数组每个元素都是一个数组,而数组本身就是一个引用类型,所以可以转换为object类型