【JavaSE四天速通|第二篇】面向对象高级篇
适合有其他语言基础想快速入门JavaSE的。用的资料是 Java入门基础视频教程 ,从中摘取了笔者认为与其他语言不同或需要重点学习的内容
本篇中讲解的都是JavaSE的面向对象的高级语法(三大特性中的继承、多态以及其他语法),需要深刻理解
day01 面向对象高级
一、静态
static读作静态,可以用来修饰成员变量,也能修饰成员方法。我们先来学习static修饰成员变量。
1.1 static修饰成员变量
Java中的成员变量按照有无static修饰分为两种:类变量、实例变量。它们的区别如下图所示:
由于静态变量是属于类的,只需要通过类名就可以调用:类名.静态变量
实例变量是属于对象的,需要通过对象才能调用:对象.实例变量
1.2 static修饰成员变量的应用场景
在实际开发中,如果某个数据只需要一份,且希望能够被共享(访问、修改),则该数据可以定义成类变量来记住。
我们看一个案例**
需求:系统启动后,要求用于类可以记住自己创建了多少个用户对象。**
public class User{public static int number;//每次创建对象时,number自增一下public User(){User.number++;}
}
public class Test{public static void main(String[] args){//创建4个对象new User();new User();new User();new User(); //查看系统创建了多少个User对象System.out.println("系统创建的User对象个数:"+User.number); // 4}
}
1.3 static修饰成员方法
成员方法根据有无static也分为两类:类方法、实例方法
static修饰成员方法的内存原理:
1.类方法:static修饰的方法,可以被类名调用,是因为它是随着类的加载而加载的;所以类名直接就可以找到static修饰的方法2.实例方法:非static修饰的方法,需要创建对象后才能调用,是因为实例方法中可能会访问实例变量,而实例变量需要创建对象后才存在。所以实例方法,必须创建对象后才能调用。
1.4 工具类
如果一个类中的方法全都是静态的,那么这个类中的方法就全都可以被类名直接调用,由于调用起来非常方便,就像一个工具一下,所以把这样的类就叫做工具类。
在补充一点,工具类里的方法全都是静态的,推荐用类名调用为了防止使用者用对象调用。我们可以把工具类的构造方法私有化。
public class MyUtils{//私有化构造方法:这样别人就不能使用构造方法new对象了private MyUtils(){}//类方法public static String createCode(int n){...}
}
1.5 static的注意事项
public class Student {static String schoolName; // 类变量double score; // 实例变量// 1、类方法中可以直接访问类的成员,不可以直接访问实例成员。public static void printHelloWorld(){// 注意:同一个类中,访问类成员,可以省略类名不写。schoolName = "黑马";printHelloWorld2();System.out.println(score); // 报错的printPass(); // 报错的ystem.out.println(this); // 报错的}// 类方法public static void printHelloWorld2(){}// 实例方法public void printPass2(){}// 实例方法// 2、实例方法中既可以直接访问类成员,也可以直接访问实例成员。// 3、实例方法中可以出现this关键字,类方法中不可以出现this关键字的public void printPass(){schoolName = "黑马2"; //对的printHelloWorld2(); //对的System.out.println(score); //对的printPass2(); //对的System.out.println(this); //对的}
}
1.6 static应用(代码块)
代码块根据有无static修饰分为两种:静态代码块、实例代码块
我们先类学习静态代码块:
public class Student {static int number = 80;static String schoolName = "黑马";// 静态代码块static {System.out.println("静态代码块执行了~~");schoolName = "黑马";}
}
静态代码块不需要创建对象就能够执行
public class Test {public static void main(String[] args) {// 目标:认识两种代码块,了解他们的特点和基本作用。System.out.println(Student.number); // 80System.out.println(Student.number);System.out.println(Student.number);System.out.println(Student.schoolName); // 黑马}
}
执行上面代码时,发现没有创建对象,静态代码块就已经执行了。
再来学习一下实例代码块
实例代码块的作用和构造器的作用是一样的,用来给对象初始化值;而且每次创建对象之前都会先执行实例代码块。
public class Student{//实例变量int age;//实例代码块:实例代码块会执行在每一个构造方法之前{System.out.println("实例代码块执行了~~");age = 18;System.out.println("有人创建了对象:" + this);}public Student(){System.out.println("无参数构造器执行了~~");}public Student(String name){System.out.println("有参数构造器执行了~~");}
}
1.7 static应用(单例设计模式)
二、继承
2.1 继承快速入门
public class A{//公开的成员public int i;public void print1(){System.out.println("===print1===");}//私有的成员private int j;private void print2(){System.out.println("===print2===");}
}
public class B extends A{public void print3(){//由于i和print1是属于父类A的公有成员,在子类中可以直接被使用System.out.println(i); //正确print1(); //正确//由于j和print2是属于父类A的私有成员,在子类中不可以被使用System.out.println(j); //错误print2();}
}
public class Test{public static void main(String[] args){B b = new B();//父类公有成员,子类对象是可以调用的System.out.println(i); //正确b.print1();//父类私有成员,子类对象时不可以调用的System.out.println(j); //错误b.print2(); //错误}
}
我们来看看继承的内存原理。
这里我们只需要关注一点:子类对象实际上是由子、父类两张设计图共同创建出来的。
所以,在子类对象的空间中,既有本类的成员,也有父类的成员(包括私有)。但是子类只能调用父类公有的成员。
2.2 继承的好处
观察代码发现,我们会发现Teacher类中和Consultant类中有相同的代码;其实像这种两个类中有相同代码时,没必要重复写。
我们可以把重复的代码提取出来,作为父类,然后让其他类继承父类就可以了,这样可以提高代码的复用性。改造后的代码如下:
关于继承的好处我们只需要记住:继承可以提高代码的复用性
2.3 权限修饰符
权限修饰符是用来限制类的成员(成员变量、成员方法、构造器…)能够被访问的范围。
每一种权限修饰符能够被访问的范围如下
public class Fu {// 1、私有:只能在本类中访问private void privateMethod(){System.out.println("==private==");}// 2、缺省:本类,同一个包下的类void method(){System.out.println("==缺省==");}// 3、protected: 本类,同一个包下的类,任意包下的子类protected void protectedMethod(){System.out.println("==protected==");}// 4、public: 本类,同一个包下的类,任意包下的子类,任意包下的任意类public void publicMethod(){System.out.println("==public==");}public void test(){//在本类中,所有权限都可以被访问到privateMethod(); //正确method(); //正确protectedMethod(); //正确publicMethod(); //正确}
}
接下来,在和Fu类同一个包下,创建一个测试类Demo,演示同一个包下可以访问到哪些权限修饰的方法。
接下来,在另一个包下创建一个Fu类的子类,演示不同包下的子类中可以访问哪些权限修饰的方法。
public class Zi extends Fu {//在不同包下的子类中,只能访问到public、protected修饰的方法public void test(){// privateMethod(); // 报错// method(); // 报错protectedMethod(); //正确publicMethod(); //正确}
}
接下来,在和Fu类不同的包下,创建一个测试类Demo2,演示一下不同包的无关类,能访问到哪些权限修饰的方法;
2.4 单继承、Object
Java语言只支持单继承,不支持多继承,但是可以多层继承。就像家族里儿子、爸爸和爷爷的关系一样:一个儿子只能有一个爸爸,不能有多个爸爸,但是爸爸也是有爸爸的。
Object类是Java中所有类的祖宗。
2.5 方法重写
什么是方法重写
当子类觉得父类方法不好用,或者无法满足父类需求时,子类可以重写一个方法名称、参数列表一样的方法,去覆盖父类的这个方法,这就是方法重写。
注意:重写后,方法的访问遵循就近原则。
public class B extends A{// 方法重写@Override // 安全,可读性好public void print1(){System.out.println("666");}// 方法重写@Overridepublic void print2(int a, int b){System.out.println("666666");}
}
- 1.重写的方法上面,可以加一个注解@Override,用于标注这个方法是复写的父类方法
- 2.子类复写父类方法时,访问权限必须大于或者等于父类方法的权限public > protected > 缺省
- 3. 重写的方法返回值类型,必须与被重写的方法返回值类型一样,或者范围更小
- 4. 私有方法、静态方法不能被重写,如果重写会报错。
方法重写的应用场景之一就是:子类重写Object的toString()方法,以便返回对象的内容。
比如:有一个Student类,这个类会默认继承Object类。
其实Object类中有一个toString()方法,直接通过Student对象调用Object的toString()方法,会得到对象的地址值。
但是,此时不想调用父类Object的toString()方法,那就可以在Student类中重新写一个toSting()方法,用于返回对象的属性值。
@Override
public String toString() {return "Student{" +"name='" + name + '\'' +", age=" + age +'}';
}
2.6 子类中访问成员的特点
我们发现继承至少涉及到两个类,而每一个类中都可能有各自的成员(成员变量、成员方法),就有可能出现子类和父类有相同成员的情况,那么在子类中访问其他成员有什么特点呢?
- 原则:在子类中访问其他成员(成员变量、成员方法),是依据就近原则的
定义一个父类,代码如下
public class F {String name = "父类名字";public void print1(){System.out.println("==父类的print1方法执行==");}
}
再定义一个子类,代码如下。有一个同名的name成员变量,有一个同名的print1成员方法;
public class Z extends F {String name = "子类名称";public void showName(){String name = "局部名称";System.out.println(name); // 局部名称}@Overridepublic void print1(){System.out.println("==子类的print1方法执行了=");}public void showMethod(){print1(); // 子类的}
}
接下来写一个测试类,观察运行结果,我们发现都是调用的子类变量、子类方法。
如果子类和父类出现同名变量或者方法,优先使用子类的;此时如果一定要在子类中使用父类的成员,可以加this或者super进行区分。
public class Z extends F {String name = "子类名称";public void showName(){String name = "局部名称";System.out.println(name); // 局部名称System.out.println(this.name); // 子类成员变量System.out.println(super.name); // 父类的成员变量}@Overridepublic void print1(){System.out.println("==子类的print1方法执行了=");}public void showMethod(){print1(); // 子类的super.print1(); // 父类的}
}
2.7 子类中访问构造器的特点
我们知道一个类中可以写成员变量、成员方法,还有构造器。在继承关系下,子类访问成员变量和成员方法的特点我们已经学过了;接下来再学习子类中访问构造器的特点。
我们先认识子类构造器的语法特点,再讲一下子类构造器的应用场景
子类中访问构造器的语法规则
- 首先,子类全部构造器,都会先调用父类构造器,再执行自己。
执行顺序,如下图按照① ② ③ 步骤执行:
子类访问构造器的应用场景 - 如果不想使用默认的
super()
方式调用父类构造器,还可以手动使用super(参数)
调用父类有参数构造器。
在本类中访问自己的构造方法
刚才我们学习了通过super()
和super(参数)
可以访问父类的构造器。有时候我们也需要访问自己类的构造器。语法如下
this(): 调用本类的空参数构造器
this(参数): 调用本类有参数的构造器
例如Student类中2个参数的构造方法可以调用本类中3个参数的构造方法
最后我们被this和super的用法在总结一下
访问本类成员:this.成员变量 //访问本类成员变量this.成员方法 //调用本类成员方法this() //调用本类空参数构造器this(参数) //调用本类有参数构造器访问父类成员:super.成员变量 //访问父类成员变量super.成员方法 //调用父类成员方法super() //调用父类空参数构造器super(参数) //调用父类有参数构造器注意:this和super访问构造方法,只能用到构造方法的第一句,否则会报错。
day02 面向对象高级
一、多态
1.1 多态概述
什么是多态?
多态是在继承、实现情况下的一种现象,表现为:对象多态、行为多态。
比如:Teacher和Student都是People的子类,代码可以写成下面的样子
1.2 多态的好处
定义方法时,使用父类类型作为形参,可以接收一切子类对象,扩展性更强,更便利。
public class Test2 {public static void main(String[] args) {// 目标:掌握使用多态的好处Teacher t = new Teacher();go(t);Student s = new Student();go(s);}//参数People p既可以接收Student对象,也能接收Teacher对象。public static void go(People p){System.out.println("开始------------------------");p.run();System.out.println("结束------------------------");}
}
1.3 类型转换
虽然多态形式下有一些好处,但是也有一些弊端。在多态形式下,不能调用子类特有的方法,比如在Teacher类中多了一个teach方法,在Student类中多了一个study方法,这两个方法在多态形式下是不能直接调用的。
多态形式下不能直接调用子类特有方法,但是转型后是可以调用的。这里所说的转型就是把父类变量转换为子类类型。格式如下:
//如果p接收的是子类对象
if(父类变量 instance 子类){//则可以将p转换为子类类型子类 变量名 = (子类)父类变量;
}
if(p instance Student){//则可以将p转换为子类类型Student s = (Student) p;s.study();
} else if (p instance Teacher) {Teacher t = (Teacher) p;t.teach();
}
如果类型转换错了,就会出现类型转换异常ClassCastException,比如把Teacher类型转换成了Student类型。关于多态转型问题,我们最终记住一句话:原本是什么类型,才能还原成什么类型
二、final关键字
2.1 final修饰符的特点
final关键字是最终的意思,可以修饰类、修饰方法、修饰变量。
- final修饰类:该类称为最终类,特点是不能被继承
- final修饰方法:该方法称之为最终方法,特点是不能被重写。
- final修饰变量:该变量只能被赋值一次。
2.2 补充知识:常量
刚刚我们学习了final修饰符的特点,在实际运用当中经常使用final来定义常量。先说一下什么是Java中的常量?
- 被 static final 修饰的成员变量,称之为常量。
- 通常用于记录系统的配置信息
public class Constant {//常量: 定义一个常量表示学校名称//为了方便在其他类中被访问所以一般还会加上public修饰符//常量命名规范:建议都采用大写字母命名,多个单词之前有_隔开public static final String SCHOOL_NAME = "传智教育";
}
关于常量的原理,同学们也可以了解一下:在程序编译后,常量会“宏替换”,出现常量的地方,全都会被替换为其记住的字面量。把代码反编译后,其实代码是下面的样子
public class FinalDemo2 {public static void main(String[] args) {//由于常量是static的所以,在使用时直接用类名就可以调用System.out.println(Constant.SCHOOL_NAME);System.out.println(Constant.SCHOOL_NAME);System.out.println(Constant.SCHOOL_NAME);}
}public class FinalDemo2 {public static void main(String[] args) {System.out.println("传智教育");System.out.println("传智教育");System.out.println("传智教育");}
}
三、抽象
3.1 认识抽象类
在Java中有一个关键字叫abstract,它就是抽象的意思,它可以修饰类也可以修饰方法。
- 被abstract修饰的类,就是抽象类
- 被abstract修饰的方法,就是抽象方法(不允许有方法体)
抽象类中不一定有抽象方法,有抽象方法的类一定是抽象类。
类该有的的成员(成员变量、成员方法、构造器),抽象类都可以有。如下面代码
// 抽象类
public abstract class A {//成员变量private String name;static String schoolName;//构造方法public A(){}//抽象方法public abstract void test();//实例方法public String getName() {return name;}public void setName(String name) {this.name = name;}
}
抽象类最主要的特点:抽象类是不能创建对象的,仅作为一种特殊的父类,让子类继承并实现
一个类继承抽象类,必须重写完抽象类的全部抽象方法,否则这个类也必须定义成抽象类
3.2 抽象类的好处
父类知道每个子类都要做某个行为,但每个子类要做的情况不一样,父类就定义成抽象方法,交给子类去重写实现,我们设计这样的抽象类,就是为了更好的支持多态。
分析需求发现,该案例中猫和狗都有名字这个属性,也都有叫这个行为,所以我们可以将共性的内容抽取成一个父类,Animal类,但是由于猫和狗叫的声音不一样,于是我们在Animal类中将叫的行为写成抽象的。代码如下
public abstract class Animal {private String name;//动物叫的行为:不具体,是抽象的public abstract void cry();public String getName() {return name;}public void setName(String name) {this.name = name;}
}
public class Cat extends Animal{@Overridepublic void cry(){System.out.println(getName() + "喵喵喵的叫~~");}
}public class Test2 {public static void main(String[] args) {// 目标:掌握抽象类的使用场景和好处.Animal a = new Dog();a.cry(); //这时执行的是Dog类的cry方法}
}
1.用抽象类可以把父类中相同的代码,包括方法声明都抽取到父类,这样能更好的支持多态,一提高代码的灵活性。2.反过来用,我们不知道系统未来具体的业务实现时,我们可以先定义抽象类,将来让子类去实现,以方便系统的扩展。
3.3 模板方法模式
设计模式是解决某一类问题的最优方案。
接下来,我们学习一种利用抽象类实现的一种设计模式。那模板方法设计模式解决什么问题呢?模板方法模式主要解决方法中存在重复代码的问题
比如A类和B类都有sing()方法,sing()方法的开头和结尾都是一样的,只是中间一段内容不一样。此时A类和B类的sing()方法中就存在一些相同的代码。
怎么解决上面的重复代码问题呢? 我们可以写一个抽象类C类,在C类中写一个doSing()的抽象方法。再写一个sing()方法,代码如下:
// 模板方法设计模式
public abstract class C {// 模板方法public final void sing(){System.out.println("唱一首你喜欢的歌:");doSing();System.out.println("唱完了!");}public abstract void doSing();
}
然后,写一个A类继承C类,复写doSing()方法,代码如下
public class A extends C{@Overridepublic void doSing() {System.out.println("我是一只小小小小鸟,想要飞就能飞的高~~~");}
}
综上所述:模板方法模式解决了多个子类中有相同代码的问题。
四、接口
接下来我们学习一个比抽象类抽象得更加彻底的一种特殊结构,叫做接口。
4.1 认识接口
public interface 接口名{//成员变量(常量)//成员方法(抽象方法)
}
public interface A{//这里public static final可以加,可以不加。public static final String SCHOOL_NAME = "黑马程序员";//这里的public abstract可以加,可以不加。public abstract void test();
}
我们发现定义好接口之后,是不能创建对象的。不过可以使用它的常量 System.out.println(A.SCHOOL_NAME);。那接口到底什么使用呢?需要我注意下面两点
- 接口是用来被类实现(implements)的,我们称之为实现类。
- 一个类是可以实现多个接口的(接口可以理解成干爹),类实现接口必须重写所有接口的全部抽象方法,否则这个类也必须是抽象类
比如,先定义一个B接口,里面有两个方法testb1(),testb2(),接着,再定义一个C接口,里面有两个方法testc1(), testc2()。然后,再写一个实现类D,同时实现B接口和C接口,此时就需要复写四个方法
// 实现类
public class D implements B, C{@Overridepublic void testb1() {}@Overridepublic void testb2() {}@Overridepublic void testc1() {}@Overridepublic void testc2() {}
}
4.2 接口的好处
主要有下面的两点
- 弥补了类单继承的不足,一个类同时可以实现多个接口。
- 让程序可以面向接口编程,这样程序员可以灵活方便的切换各种业务实现。
假设有一个Studnet学生类,还有一个Driver司机的接口,还有一个Singer歌手的接口。现在要写一个A类,想让他既是学生,偶然也是司机能够开车,偶尔也是歌手能够唱歌。
class Student{}interface Driver{void drive();
}interface Singer{void sing();
}//A类是Student的子类,同时也实现了Dirver接口和Singer接口
class A extends Student implements Driver, Singer{@Overridepublic void drive() {}@Overridepublic void sing() {}
}public class Test {public static void main(String[] args) {//想唱歌的时候,A类对象就表现为Singer类型Singer s = new A();s.sing();//想开车的时候,A类对象就表现为Driver类型Driver d = new A();d.drive();}
}
4.3 接口的案例
首先我们写一个学生类,用来描述学生的相关信息
接着,写一个StudentOperator接口,表示学生信息管理系统的两个功能。
public interface StudentOperator {void printAllInfo(ArrayList<Student> students);void printAverageScore(ArrayList<Student> students);
}
然后,写一个StudentOperator接口的实现类StudentOperatorImpl1,采用第1套方案对业务进行实现。
接着,再写一个StudentOperator接口的实现类StudentOperatorImpl2,采用第2套方案对业务进行实现。
再写一个班级管理类ClassManager,在班级管理类中使用StudentOperator的实现类StudentOperatorImpl1对学生进行操作
public class ClassManager {private ArrayList<Student> students = new ArrayList<>();private StudentOperator studentOperator = new StudentOperatorImpl1();public ClassManager(){students.add(new Student("迪丽热巴", '女', 99));students.add(new Student("古力娜扎", '女', 100));students.add(new Student("马尔扎哈", '男', 80));students.add(new Student("卡尔扎巴", '男', 60));}// 打印全班全部学生的信息public void printInfo(){studentOperator.printAllInfo(students);}// 打印全班全部学生的平均分public void printScore(){studentOperator.printAverageScore(students);}
}
最后,再写一个测试类Test,在测试类中使用ClassMananger完成班级学生信息的管理。
如果想切换班级管理系统的业务功能,随时可以将StudentOperatorImpl1切换为StudentOperatorImpl2。
4.4 接口JDK8的新特性
从JDK8开始,接口中新增的三种方法形式。我们看一下这三种方法分别有什么特点?
public interface A {/*** 1、默认方法:必须使用default修饰。默认会被public修饰* 实例方法:对象的方法,必须使用实现类的对象来访问。*/default void test1(){System.out.println("===默认方法==");test2();}/*** 2、私有方法:必须使用private修饰。(JDK 9开始才支持的)* 实例方法:对象的方法。私有的因此得在这个接口里面进行访问,比如在默认方法中访问*/private void test2(){System.out.println("===私有方法==");}/*** 3、静态方法:必须使用static修饰。默认会被public修饰*/static void test3(){System.out.println("==静态方法==");}void test4();void test5();default void test6(){}
}
接下来我们写一个B类,实现A接口。B类作为A接口的实现类,只需要重写抽象方法就尅了,对于默认方法不需要子类重写。
public class B implements A{@Overridepublic void test4() {}@Overridepublic void test5() {}
}
public class Test {public static void main(String[] args) {// 目标:掌握接口新增的三种方法形式B b = new B();b.test1(); //默认方法使用对象调用// b.test2(); //A接口中的私有方法,B类调用不了A.test3(); //静态方法,使用接口名调用}
}
4.5 接口的其他细节
一个接口可以继承多个接口
public class Test {public static void main(String[] args) {// 目标:理解接口的多继承。}
}interface A{void test1();
}
interface B{void test2();
}
interface C{}//比如:D接口继承C、B、A
interface D extends C, B, A{}//E类在实现D接口时,必须重写D接口、以及其父类中的所有抽象方法。
class E implements D{@Overridepublic void test1() {}@Overridepublic void test2() {}
}
接口除了上面的多继承特点之外,在多实现、继承和实现并存时,有可能出现方法名冲突的问题,需要了解怎么解决(仅仅只是了解一下,实际上工作中几乎不会出现这种情况)
1.一个接口继承多个接口,如果多个接口中存在相同的方法声明,则此时不支持多继承
2.一个类实现多个接口,如果多个接口中存在相同的方法声明,则此时不支持多实现
3.一个类继承了父类,又同时实现了接口,父类中和接口中有同名的默认方法,实现类会有限使用父类的方法
4.一个类实现类多个接口,多个接口中有同名的默认方法,则这个类必须重写该方法。
day03 面向对象高级
一、内部类
内部类是类中的五大成分之一(成员变量、方法、构造器、内部类、代码块),如果一个类定义在另一个类的内部,这个类就是内部类。
当一个类的内部,包含一个完整的事物,且这个事物没有必要单独设计时,就可以把这个事物设计成内部类。
比如:汽车、的内部有发动机,发动机是包含在汽车内部的一个完整事物,可以把发动机设计成内部类。
内部类有四种形式,分别是成员内部类、静态内部类、局部内部类、匿名内部类。
1.1 成员内部类
成员内部类就是类中的一个普通成员,类似于成员变量、成员方法。
public class Outer {private int age = 99;public static String a="黑马";// 成员内部类public class Inner{private String name;private int age = 88;//在内部类中既可以访问自己类的成员,也可以访问外部类的成员public void test(){System.out.println(age); //88System.out.println(a); //黑马int age = 77;System.out.println(age); //77System.out.println(this.age); //88System.out.println(Outer.this.age); //99}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;}}
}
成员内部类如何创建对象,格式如下
//外部类.内部类 变量名 = new 外部类().new 内部类();
Outer.Inner in = new Outer().new Inner();
//调用内部类的方法
in.test();
总结一下内部类访问成员的特点
- 既可以访问内部类成员、也可以访问外部类成员
- 如果内部类成员和外部类成员同名,可以使用**
类名.this.成员
**区分
1.2 静态内部类
静态内部类,其实就是在成员内部类的前面加了一个static关键字。静态内部类属于外部类自己持有。
public class Outer {private int age = 99;public static String schoolName="黑马";// 静态内部类public static class Inner{//静态内部类访问外部类的静态变量,是可以的;//静态内部类访问外部类的实例变量,是不行的public void test(){System.out.println(schoolName); //99//System.out.println(age); //报错}}
}
静态内部类创建对象时,需要使用外部类的类名调用。
//格式:外部类.内部类 变量名 = new 外部类.内部类();
Outer.Inner in = new Outer.Inner();
in.test();
1.3 局部内部类
局部内部类是定义在方法中的类,和局部变量一样,只能在方法中有效。所以局部内部类的局限性很强,一般在开发中是不会使用的。
public class Outer{public void test(){//局部内部类class Inner{public void show(){System.out.println("Inner...show");}}//局部内部类只能在方法中创建对象,并使用Inner in = new Inner();in.show();}
}
1.4 匿名内部类
匿名内部类是一种特殊的局部内部类;所谓匿名,指的是程序员不需要为这个类声明名字。
下面就是匿名内部类的格式:
new 父类/接口(参数值){@Override重写父类/接口的方法;
}
匿名内部类本质上是一个没有名字的子类对象、或者接口的实现类对象。
比如,先定义一个Animal抽象类,里面定义一个cry()方法,表示所有的动物有叫的行为,但是因为动物还不具体,cry()这个行为并不能具体化,所以写成抽象方法。
public abstract class Animal{public abstract void cry();
}
接下来,我想要在不定义子类的情况下创建Animal的子类对象,就可以使用匿名内部类
public class Test{public static void main(String[] args){//这里后面new 的部分,其实就是一个Animal的子类对象//这里隐含的有多态的特性: Animal a = Animal子类对象;Animal a = new Animal(){@Overridepublic void cry(){System.out.println("猫喵喵喵的叫~~~");}}a.eat(); //直线上面重写的cry()方法}
}
需要注意的是,匿名内部类在编写代码时没有名字,编译后系统会为自动为匿名内部类生产字节码,字节码的名称会以外部类$1.class
的方法命名
匿名内部类的作用:简化了创建子类对象、实现类对象的书写格式。
我们再来看一下匿名内部类在实际中的应用场景。其实一般我们会主动的使用匿名内部类。
只有在调用方法时,当方法的形参是一个接口或者抽象类,为了简化代码书写,而直接传递匿名内部类对象给方法。这样就可以少写一个类。比如,看下面代码
public interface Swimming{public void swim();
}
public class Test{public static void main(String[] args){Swimming s1 = new Swimming(){public void swim(){System.out.println("狗刨飞快");}};go(s1);Swimming s1 = new Swimming(){public void swim(){System.out.println("猴子游泳也还行");}};go(s1);}//形参是Swimming接口,实参可以接收任意Swimming接口的实现类对象public static void go(Swimming s){System.out.println("开始~~~~~~~~");s.swim();System.out.println("结束~~~~~~~~");}
}
二、枚举
2.1 认识枚举
枚举是一种特殊的类,它的格式是:
public enum 枚举类名{枚举项1,枚举项2,枚举项3;
}
其实枚举项就表示枚举类的对象,只是这些对象在定义枚举类时就预先写好了,以后就只能用这几个固定的对象。
想要获取枚举类中的枚举项,只需要用类名调用就可以了
public enum A{X,Y,Z;
}
public class Test{public static void main(String[] args){//获取枚举A类的,枚举项A a1 = A.X;A a2 = A.Y;A a3 = A.Z;}
}
我们会看到,枚举类A是用class定义的,说明枚举确实是一个类,而且X,Y,Z都是A类的对象;而且每一个枚举项都是被public static final
修饰,所以被可以类名调用,而且不能更改。
既然枚举是一个类的话,我们能不能在枚举类中定义构造器、成员变量、成员方法呢?答案是可以的。
三、泛型
3.1 认识泛型
所谓泛型指的是,在定义类、接口、方法时,同时声明了一个或者多个类型变量(如:<E>
),称为泛型类、泛型接口、泛型方法、它们统称为泛型。
比如我们前面学过的ArrayList类就是一个泛型类,我们可以打开API文档看一下ArrayList类的声明。
ArrayList集合的设计者在定义ArrayList集合时,就已经明确ArrayList集合时给别人装数据用的,但是别人用ArrayList集合时候,装什么类型的数据他不知道,所以就用一个<E>
表示元素的数据类型。
当别人使用ArrayList集合创建对象时,new ArrayList<String>
就表示元素为String类型,new ArrayList<Integer>
表示元素为Integer类型。
我们总结一下泛型的作用、本质:
- 泛型的好处:在编译阶段可以避免出现一些非法的数据。
- 泛型的本质:把具体的数据类型传递给类型变量。
3.2 自定义泛型类
自定义泛型类的格式如下
//这里的<T,W>其实指的就是类型变量,可以是一个,也可以是多个。
public class 类名<T,W>{}
3.3 自定义泛型接口
泛型接口其实指的是在接口中把不确定的数据类型用<类型变量>
表示。定义格式如下:
//这里的类型变量,一般是一个字母,比如<E>
public interface 接口名<类型变量>{}
比如,我们现在要做一个系统要处理学生和老师的数据,需要提供2个功能,保存对象数据、根据名称查询数据,要求:这两个功能处理的数据既能是老师对象,也能是学生对象。
3.4 泛型方法
public <泛型变量,泛型变量> 返回值类型 方法名(形参列表){}
3.5 泛型限定
接着,我们来学习一个泛型的特殊用法,叫做泛型限定。泛型限定的意思是对泛型的数据类型进行范围的限制。有如下的三种格式
- <?> 表示任意类型
- <? extends 数据类型> 表示指定类型或者指定类型的子类
- <? super 数据类型> 表示指定类型或者指定类型的父类
3.6 泛型擦除
也就是说泛型只能编译阶段有效,一旦编译成字节码,字节码中是不包含泛型的。而且泛型只支持引用数据类型,不支持基本数据类型。
把下面的代码的字节码进行反编译
下面是反编译之后的代码,我们发现ArrayList后面没有泛型
四、常用API
4.1 Object类
Object类是Java中所有类的祖宗类,因此,Java中所有类的对象都可以直接使用Object类中提供的一些方法。
我们先来学习toString()方法。
public String toString()调用toString()方法可以返回对象的字符串表示形式。默认的格式是:“包名.类名@哈希值16进制”
接下来,我们学习一下Object类的equals方法
public boolean equals(Object o)判断此对象与参数对象是否"相等"
public class Test{public static void main(String[] args){Student s1 = new Student("赵薇",23);Student s2 = new Student("赵薇",23);//equals本身也是比较对象的地址,和"=="没有区别System.out.println(s1.equals(s2)); //false//"=="比较对象的地址System.out.println(s1==s2); //false}
}
总结
public String toString()返回对象的字符串表示形式。默认的格式是:“包名.类名@哈希值16进制”【子类重写后,返回对象的属性值】public boolean equals(Object o)判断此对象与参数对象是否"相等"。默认比较对象的地址值,和"=="没有区别【子类重写后,比较对象的属性值】
接下来,我们学习Object类的clone()方法,克隆。意思就是某一个对象调用这个方法,这个方法会复制一个一模一样的新对象,并返回。
public Object clone()克隆当前对象,返回一个新对象
想要调用clone()方法,必须让被克隆的类实现Cloneable接口。如我们准备克隆User类的对象,代码如下
public class User implements Cloneable{private String id; //编号private String username; //用户名private String password; //密码private double[] scores; //分数public User() {}public User(String id, String username, String password, double[] scores) {this.id = id;this.username = username;this.password = password;this.scores = scores;}//...get和set...方法自己加上@Overrideprotected Object clone() throws CloneNotSupportedException {return super.clone();}
}
public class Test {public static void main(String[] args) throws CloneNotSupportedException {User u1 = new User(1,"zhangsan","wo666",new double[]{99.0,99.5});//调用方法克隆得到一个新对象User u2 = (User) u1.clone();System.out.println(u2.getId());System.out.println(u2.getUsername());System.out.println(u2.getPassword());System.out.println(u2.getScores()); }
}
我们发现,克隆得到的对象u2它的属性值和原来u1对象的属性值是一样的。注意,连里面的数组都是指向同一个地址
上面演示的克隆方式,是一种浅克隆的方法,浅克隆的意思:拷贝出来的对象封装的数据与原对象封装的数据一模一样(引用类型拷贝的是地址值)。如下图所示
还有一种拷贝方式,称之为深拷贝,拷贝原理如下图所示
下面演示一下深拷贝User对象
public class User implements Cloneable{private String id; //编号private String username; //用户名private String password; //密码private double[] scores; //分数public User() {}public User(String id, String username, String password, double[] scores) {this.id = id;this.username = username;this.password = password;this.scores = scores;}//...get和set...方法自己加上@Overrideprotected Object clone() throws CloneNotSupportedException {//先克隆得到一个新对象User u = (User) super.clone();//再将新对象中的引用类型数据,再次克隆u.scores = u.scores.clone();return u;}
}
4.2 Objects类
Objects是一个工具类,提供了一些方法可以对任意对象进行操作。主要方法如下
4.3 基本类型包装类
为什么要学习包装类呢?因为在Java中有一句很经典的话,万物皆对象。Java中的8种基本数据类型还不是对象,所以要把它们变成对象,变成对象之后,可以提供一些方法对数据进行操作。
Java中8种基本数据类型都用一个包装类与之对一个,如下图所示
我们学习包装类,主要学习两点:
-
- 创建包装类的对象方式、自动装箱和拆箱的特性;
-
- 利用包装类提供的方法对字符串和基本类型数据进行相互转换
我们先来学习,创建包装类对象的方法,以及包装类的一个特性叫自动装箱和自动拆箱。我们以Integer为例,其他的可以自己学,都是类似的。
//1.创建Integer对象,封装基本类型数据10
Integer a = new Integer(10);//2.使用Integer类的静态方法valueOf(数据)
Integer b = Integer.valueOf(10);//3.还有一种自动装箱的写法(意思就是自动将基本类型转换为引用类型)
Integer c = 10;//4.有装箱肯定还有拆箱(意思就是自动将引用类型转换为基本类型)
int d = c;//5.装箱和拆箱在使用集合时就有体现
ArrayList<Integer> list = new ArrayList<>();
//添加的元素是基本类型,实际上会自动装箱为Integer类型
list.add(100);
//获取元素时,会将Integer类型自动拆箱为int类型
int e = list.get(0);
在开发中,经常使用包装类对字符串和基本类型数据进行相互转换。
(1)把字符串转换为数值型数据:包装类.parseXxx(字符串)
public static int parseInt(String s)把字符串转换为基本数据类型
(2)将数值型数据转换为字符串:包装类.valueOf(数据);
public static String valueOf(int a)把基本类型数据转换为
//1.字符串转换为数值型数据
String ageStr = "29";
int age1 = Integer.parseInt(ageStr);String scoreStr = 3.14;
double score = Double.prarseDouble(scoreStr);//2.整数转换为字符串,以下几种方式都可以(挑中你喜欢的记一下)
Integer a = 23;
String s1 = Integer.toString(a);
String s2 = a.toString();
String s3 = a+"";
String s4 = String.valueOf(a);