12——面向对象实用类
各位同学,前面两天我们已经把面向对象最主要的内容学习完了,剩下的这些语法知识学完,那么Java语法知识就算全齐活了。
今天学习的内容同学们学习起来会更轻松一些,有一些语法知识只需要了解一下就可以了,因为实际工作用得并不多。
我们先来了解第一个语法知识,内部类。
一、内部类
内部类是类中的五大成分之一(成员变量、方法、构造器、内部类、代码块),如果一个类定义在另一个类的内部,这个类就是内部类。
当一个类的内部,包含一个完整的事物,且这个事物没有必要单独设计时,就可以把这个事物设计成内部类。
比如:汽车、的内部有发动机,发动机是包含在汽车内部的一个完整事物,可以把发动机设计成内部类。
1 2 3 4 5 6
| public class Car{ public class Engine{ } }
|
内部类有四种形式,分别是成员内部类、静态内部类、局部内部类、匿名内部类。
我们先来学习成员内部类
1.1 成员内部类
成员内部类就是类中的一个普通成员,类似于成员变量、成员方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| 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); System.out.println(a);
int age = 77; System.out.println(age); System.out.println(this.age); System.out.println(Outer.this.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; } } }
|
成员内部类如何创建对象,格式如下
1 2 3 4
| Outer.Inner in = new Outer().new Inner();
in.test();
|
总结一下内部类访问成员的特点
- 既可以访问内部类成员、也可以访问外部类成员
- 如果内部类成员和外部类成员同名,可以使用**
类名.this.成员
**区分
1.2 静态内部类
静态内部类,其实就是在成员内部类的前面加了一个static关键字。静态内部类属于外部类自己持有。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| public class Outer { private int age = 99; public static String schoolName="黑马";
public static class Inner{ public void test(){ System.out.println(schoolName); } } }
|
静态内部类创建对象时,需要使用外部类的类名调用。
1 2 3
| Outer.Inner in = new Outer.Inner(); in.test();
|
1.3 局部内部类
局部内部类是定义在方法中的类,和局部变量一样,只能在方法中有效。所以局部内部类的局限性很强,一般在开发中是不会使用的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| public class Outer{ public void test(){ class Inner{ public void show(){ System.out.println("Inner...show"); } } Inner in = new Inner(); in.show(); } }
|
1.4 匿名内部类
1.4.1 认识匿名内部类,基本使用
各位同学,接下来学习一种再实际开发中用得最多的一种内部类,叫匿名内部类。相比于前面几种内部类,匿名内部类就比较重要的。
我们还是先认识一下什么是匿名内部类?
匿名内部类是一种特殊的局部内部类;所谓匿名,指的是程序员不需要为这个类声明名字。
下面就是匿名内部类的格式:
1 2 3 4
| new 父类/接口(参数值){ @Override 重写父类/接口的方法; }
|
匿名内部类本质上是一个没有名字的子类对象、或者接口的实现类对象。
比如,先定义一个Animal抽象类,里面定义一个cry()方法,表示所有的动物有叫的行为,但是因为动物还不具体,cry()这个行为并不能具体化,所以写成抽象方法。
1 2 3
| public abstract class Animal{ public abstract void cry(); }
|
接下来,我想要在不定义子类的情况下创建Animal的子类对象,就可以使用匿名内部类
1 2 3 4 5 6 7 8 9 10 11 12 13
| public class Test{ public static void main(String[] args){ Animal a = new Animal(){ @Override public void cry(){ System.out.println("猫喵喵喵的叫~~~"); } } a.eat(); } }
|
需要注意的是,匿名内部类在编写代码时没有名字,编译后系统会为自动为匿名内部类生产字节码,字节码的名称会以外部类$1.class
的方法命名

匿名内部类的作用:简化了创建子类对象、实现类对象的书写格式。
1.4.2 匿名内部类的应用场景
学习完匿名内部类的基本使用之后,我们再来看一下匿名内部类在实际中的应用场景。其实一般我们会主动的使用匿名内部类。
只有在调用方法时,当方法的形参是一个接口或者抽象类,为了简化代码书写,而直接传递匿名内部类对象给方法。这样就可以少写一个类。比如,看下面代码
1 2 3
| public interface Swimming{ public void swim(); }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| 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); } public static void go(Swimming s){ System.out.println("开始~~~~~~~~"); s.swim(); System.out.println("结束~~~~~~~~"); } }
|
二、枚举
2.1 认识枚举
2.1.1 认识枚举、枚举的原理
同学们,接下来我们学习一个新的知识点,枚举。枚举是我们以后在项目开发中偶尔会用到的知识。话不多说,我们还是先来认识一下枚举。
枚举是一种特殊的类,它的格式是:
1 2 3
| public enum 枚举类名{ 枚举项1,枚举项2,枚举项3; }
|
其实枚举项就表示枚举类的对象,只是这些对象在定义枚举类时就预先写好了,以后就只能用这几个固定的对象。
我们用代码演示一下,定义一个枚举类A,在枚举类中定义三个枚举项X, Y, Z
1 2 3
| public enum A{ X,Y,Z; }
|
想要获取枚举类中的枚举项,只需要用类名调用就可以了
1 2 3 4 5 6 7 8
| public class Test{ public static void main(String[] args){ A a1 = A.X; A a2 = A.Y; A a3 = A.Z; } }
|
刚才说,枚举项实际上是枚举类的对象,这一点其实可以通过反编译的形式来验证(需要用到反编译的命令,这里不能直接将字节码拖进idea反编译)

我们会看到,枚举类A是用class定义的,说明枚举确实是一个类,而且X,Y,Z都是A类的对象;而且每一个枚举项都是被public static final
修饰,所以被可以类名调用,而且不能更改。
2.1.2 枚举深入
既然枚举是一个类的话,我们能不能在枚举类中定义构造器、成员变量、成员方法呢?答案是可以的。来看一下代码吧
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| public enum A{ X,Y,Z("张三"); public A(){ } private String name; public A(String name){ this.name=name; } public String getName(){ return name; } ... }
|
虽然枚举类中可以像类一样,写一些类的其他成员,但是一般不会这么写,如果你真要这么干的话,到不如直接写普通类来的直接。
2.2 枚举的应用场景
刚才我们认识了一下什么是枚举,接下来我们看一下枚举在实际中的运用,枚举的应用场景是这样的:枚举一般表示一组信息,然后作为参数进行传输。
我们来看一个案例。比如我们现在有这么一个应用,用户进入应用时,需要让用户选择是女生、还是男生,然后系统会根据用户选择的是男生,还是女生推荐不同的信息给用户观看。

这里我们就可以先定义一个枚举类,用来表示男生、或者女生
1 2 3
| public class Constant{ BOY,GRIL }
|
再定义一个测试类,完成用户进入系统后的选择
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| public class Test{ public static void main(String[] args){ provideInfo(Constant.BOY); } public static void provideInfo(Constant c){ switch(c){ case BOY: System.out.println("展示一些信息给男生看"); break; case GRIL: System.out.println("展示一些信息给女生看"); break; } } }
|
最终再总结一下枚举的应用场景:枚举一般表示几个固定的值,然后作为参数进行传输。
三、泛型
3.1 认识泛型
所谓泛型指的是,在定义类、接口、方法时,同时声明了一个或者多个类型变量(如:),称为泛型类、泛型接口、泛型方法、它们统称为泛型。
比如我们前面学过的ArrayList类就是一个泛型类,我们可以打开API文档看一下ArrayList类的声明。

ArrayList集合的设计者在定义ArrayList集合时,就已经明确ArrayList集合时给别人装数据用的,但是别人用ArrayList集合时候,装什么类型的数据他不知道,所以就用一个<E>
表示元素的数据类型。
当别人使用ArrayList集合创建对象时,new ArrayList<String>
就表示元素为String类型,new ArrayList<Integer>
表示元素为Integer类型。

我们总结一下泛型的作用、本质:
3.2 自定义泛型类
接下来我们学习一下自定义泛型类,但是有一些话需要给大家提前交代一下:泛型类,在实际工作中一般都是源代码中写好,我们直接用的,就是ArrayList这样的,自己定义泛型类是非常少的。
自定义泛型类的格式如下
1 2 3 4
| public class 类名<T,W>{ }
|
接下来,我们自己定义一个MyArrayList泛型类,模拟一下自定义泛型类的使用。注意这里重点仅仅只是模拟泛型类的使用,所以方法中的一些逻辑是次要的,也不会写得太严谨。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
|
public class MyArrayList<E>{ private Object[] array = new Object[10]; private int index; public void add(E e){ array[index]=e; index++; } public E get(int index){ return (E)array[index]; } }
|
接下来,我们写一个测试类,来测试自定义的泛型类MyArrayList是否能够正常使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| public class Test{ public static void main(String[] args){ MyArrayList<String> list = new MyArrayList<>(); list.add("张三"); list.add("李四"); MyArrayList<Integer> list1 = new MyArrayList<>(); list.add(100); list.add(200); } }
|
关于自定义泛型类,你们把这个案例理解,对于初学者来说,就已经非常好了。
3.3 自定义泛型接口
在上一节中,我们已经学习了自定义泛型类,接下来我们学习一下泛型接口。泛型接口其实指的是在接口中把不确定的数据类型用<类型变量>
表示。定义格式如下:
1 2 3 4
| public interface 接口名<类型变量>{ }
|
比如,我们现在要做一个系统要处理学生和老师的数据,需要提供2个功能,保存对象数据、根据名称查询数据,要求:这两个功能处理的数据既能是老师对象,也能是学生对象。
首先我们得有一个学生类和老师类
1 2 3
| public class Teacher{
}
|
1 2 3
| public class Student{ }
|
我们定义一个Data<T>
泛型接口,T表示接口中要处理数据的类型。
1 2 3 4 5
| public interface Data<T>{ public void add(T t); public ArrayList<T> getByName(String name); }
|
接下来,我们写一个处理Teacher对象的接口实现类
1 2 3 4 5 6 7 8 9 10 11
|
public class TeacherData implements Data<Teacher>{ public void add(Teacher t){ } public ArrayList<Teacher> getByName(String name){ } }
|
接下来,我们写一个处理Student对象的接口实现类
1 2 3 4 5 6 7 8 9 10 11
|
public class StudentData implements Data<Student>{ public void add(Student t){ } public ArrayList<Student> getByName(String name){ } }
|
再啰嗦几句,在实际工作中,一般也都是框架底层源代码把泛型接口写好,我们实现泛型接口就可以了。
3.4 泛型方法
同学们,接下来我们学习一下泛型方法。下面就是泛型方法的格式
1 2 3
| public <泛型变量,泛型变量> 返回值类型 方法名(形参列表){ }
|
下图中在返回值类型和修饰符之间有定义的才是泛型方法。

接下我们看一个泛型方法的案例
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| public class Test{ public static void main(String[] args){ String rs = test("test"); Dog d = test(new Dog()); } public static <T> test(T t){ return t; } }
|
3.5 泛型限定
接着,我们来学习一个泛型的特殊用法,叫做泛型限定。泛型限定的意思是对泛型的数据类型进行范围的限制。有如下的三种格式
- > 表示任意类型
- extends 数据类型> 表示指定类型或者指定类型的子类
- super 数据类型> 表示指定类型或者指定类型的父类
下面我们演示一下,假设有Car作为父类,BENZ,BWM两个类作为Car的子类,代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| class Car{} class BENZ extends Car{} class BWN extends Car{}
public class Test{ public static void main(String[] args){ ArrayList<BWM> list1 = new ArrayList<>(); ArrayList<Benz> list2 = new ArrayList<>(); ArrayList<String> list3 = new ArrayList<>(); test1(list1); test1(list2); test1(list3); ArrayList<Car> list4 = new ArrayList<>(); ArrayList<BWM> list5 = new ArrayList<>(); test2(list4); test2(list5); ArrayList<Car> list6 = new ArrayList<>(); ArrayList<Object> list7 = new ArrayList<>(); test3(list6); test3(list7); } public static void test1(ArrayList<?> list){ } public static void test2(ArrayList<? extends Car> list){ } public static void test3(ArrayList<? super Car> list){ } }
|
3.6 泛型擦除
最后,关于泛型还有一个特点需要给同学们介绍一下,就是泛型擦除。什么意思呢?也就是说泛型只能编译阶段有效,一旦编译成字节码,字节码中是不包含泛型的。而且泛型只支持引用数据类型,不支持基本数据类型。
把下面的代码的字节码进行反编译

下面是反编译之后的代码,我们发现ArrayList后面没有泛型

四、常用API
各位同学,恭喜大家,到目前位置我们关于面向对象的语法知识就全部学习完了。接下来我们就可以拿着这些语法知识,去学习一个一个的API方法,掌握的API方法越多,那么Java的编程能力就越强。
API(Application Programming interface)意思是应用程序编程接口,说人话就是Java帮我们写好的一些程序,如:类、方法等,我们直接拿过来用就可以解决一些问题。

我们要学习那些API呢?把下面一种图中的所有类的常用方法学会了,那我们JavaSE进阶的课程就算你全学会了。

很多初学者给我反应的问题是,这些API一听就会,但是就是记住不!送同学们一句话,
“千里之行始于足下,多记、多查、多些代码、孰能生巧!”

4.1 Object类
各位小伙伴,我们要学习的第一个API就是Object类。Object类是Java中所有类的祖宗类,因此,Java中所有类的对象都可以直接使用Object类中提供的一些方法。
按照下图的提示,可以搜索到你想要找的类

我们找到Object类的下面两个方法

我们先来学习toString()方法。
1 2 3
| public String toString() 调用toString()方法可以返回对象的字符串表示形式。 默认的格式是:“包名.类名@哈希值16进制”
|
假设有一个学生类如下
1 2 3 4 5 6 7 8 9
| public class Student{ private String name; private int age; public Student(String name, int age){ this.name=name; this.age=age; } }
|
再定义一个测试类
1 2 3 4 5 6
| public class Test{ public static void main(String[] args){ Student s1 = new Student("赵敏",23); System.out.println(s1.toString()); } }
|
打印结果如下

如果,在Student类重写toString()方法,那么我们可以返回对象的属性值,代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| public class Student{ private String name; private int age; public Student(String name, int age){ this.name=name; this.age=age; } @Override public String toString(){ return "Student{name=‘"+name+"’, age="+age+"}"; } }
|
运行测试类,结果如下

4.1.2 equals(Object o)方法
接下来,我们学习一下Object类的equals方法
1 2
| public boolean equals(Object o) 判断此对象与参数对象是否"相等"
|
我们写一个测试类,测试一下
1 2 3 4 5 6 7 8 9 10 11
| public class Test{ public static void main(String[] args){ Student s1 = new Student("赵薇",23); Student s2 = new Student("赵薇",23); System.out.println(s1.equals(s2)); System.out.println(s1==s2); } }
|
但是如果我们在Student类中,把equals方法重写了,就按照对象的属性值进行比较
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| public class Student{ private String name; private int age; public Student(String name, int age){ this.name=name; this.age=age; } @Override public String toString(){ return "Student{name=‘"+name+"’, 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;
if (age != student.age) return false; return name != null ? name.equals(student.name) : student.name == null; } }
|
再运行测试类,效果如下

总结一下Object的toString方法和equals方法
1 2 3 4 5 6 7
| public String toString() 返回对象的字符串表示形式。默认的格式是:“包名.类名@哈希值16进制” 【子类重写后,返回对象的属性值】 public boolean equals(Object o) 判断此对象与参数对象是否"相等"。默认比较对象的地址值,和"=="没有区别 【子类重写后,比较对象的属性值】
|
4.1.3 clone() 方法
接下来,我们学习Object类的clone()方法,克隆。意思就是某一个对象调用这个方法,这个方法会复制一个一模一样的新对象,并返回。
1 2
| public Object clone() 克隆当前对象,返回一个新对象
|
想要调用clone()方法,必须让被克隆的类实现Cloneable接口。如我们准备克隆User类的对象,代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| 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; }
@Override protected Object clone() throws CloneNotSupportedException { return super.clone(); } }
|
接着,我们写一个测试类,克隆User类的对象。并观察打印的结果
1 2 3 4 5 6 7 8 9 10 11
| 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对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| 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; }
@Override protected Object clone() throws CloneNotSupportedException { User u = (User) super.clone(); u.scores = u.scores.clone(); return u; } }
|

4.2 Objects类
Objects是一个工具类,提供了一些方法可以对任意对象进行操作。主要方法如下

下面写代码演示一下这几个方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| public class Test{ public static void main(String[] args){ String s1 = null; String s2 = "itheima"; System.out.println(s1.equals(s2)); System.out.println(Objects.equals(s1,s2)); System.out.println(Objects.isNull(s1)); System.out.println(s1==null); System.out.println(Objects.nonNull(s2)); System.out.println(s2!=null); } }
|
4.3 基本类型包装类
同学们,接下来我们学习一下包装类。为什么要学习包装类呢?因为在Java中有一句很经典的话,万物皆对象。Java中的8种基本数据类型还不是对象,所以要把它们变成对象,变成对象之后,可以提供一些方法对数据进行操作。
Java中8种基本数据类型都用一个包装类与之对一个,如下图所示

我们学习包装类,主要学习两点:
- 创建包装类的对象方式、自动装箱和拆箱的特性;
- 利用包装类提供的方法对字符串和基本类型数据进行相互转换
4.2.1 创建包装类对象
我们先来学习,创建包装类对象的方法,以及包装类的一个特性叫自动装箱和自动拆箱。我们以Integer为例,其他的可以自己学,都是类似的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| Integer a = new Integer(10);
Integer b = Integer.valueOf(10);
Integer c = 10;
int d = c;
ArrayList<Integer> list = new ArrayList<>();
list.add(100);
int e = list.get(0);
|
4.2.2 包装类数据类型转换
在开发中,经常使用包装类对字符串和基本类型数据进行相互转换。
- 把字符串转换为数值型数据:包装类.parseXxx(字符串)
1 2
| public static int parseInt(String s) 把字符串转换为基本数据类型
|
- 将数值型数据转换为字符串:包装类.valueOf(数据);
1 2
| public static String valueOf(int a) 把基本类型数据转换为
|
1 2 3 4 5 6 7 8 9 10 11 12 13
| String ageStr = "29"; int age1 = Integer.parseInt(ageStr);
String scoreStr = 3.14; double score = Double.prarseDouble(scoreStr);
Integer a = 23; String s1 = Integer.toString(a); String s2 = a.toString(); String s3 = a+""; String s4 = String.valueOf(a);
|