一、静态变量(类变量)
1、什么是静态变量
在Java中使用static关键字修饰的变量就是静态变量
2、特点
静态变量和静态方法的特点在于,其与类直接相关,被所有该类的对象共享。
也就是说这个类中的静态变量变化以后,所有该类的对象中的静态变量都会相应同步变化。
举例先来看非静态变量:
package static_;
public class Static_ {
public static void main(String[] args) {
Player player1 = new Player();
Player player2 = new Player();
System.out.println("player1初始的count = " + player1.count);
player1.addCount();
System.out.println("player1现在的count = " + player1.count);
System.out.println("player2现在的count = " + player2.count);
}
}
class Player{
//普通属性count
int count = 0;
//增加count的方法
public void addCount(){
this.count++;
}
}
这里我定义了Player类,里面有一个非静态的属性(普通属性)count 初始化为0。
然后创建一个方法用来增加count。
创建两个对象player1和player2,调用player1的方法,让count增加1
在调用这个方法后,我来打印一下:
显然,player1调用的方法不会影响player2。
现在再来看一下静态变量:
package static_;
public class Static_ {
public static void main(String[] args) {
Player player1 = new Player();
Player player2 = new Player();
System.out.println("player1初始的count = " + player1.count);
player1.addCount();
System.out.println("player1现在的count = " + player1.count);
System.out.println("player2现在的count = " + player2.count);
}
}
class Player{
//静态属性count
static int count;
//增加count的方法
public void addCount(){
count++;
}
}
结果:
可以看到,虽然只有player1调用了count增加的方法,但是player2的count也增加了,这就是静态变量的特点。被所有对象共享。
(值得一提,静态变量跟普通变量一样也可以有访问修饰符)
3、作用
可以用于统计人数等,便于程序开发。
如上面的例子中,Player是我们游戏中的玩家,那么静态变量就很有用了,我们可以这样设置:我们每创建一个新玩家就让count ++,也就是在构造器中让count++,这样就可以统计当前游戏的总人数:
package static_;
public class Static_ {
public static void main(String[] args) {
Player player1 = new Player();
Player player2 = new Player();
}
}
class Player{
//静态属性count
static int count = 0;
//构造器:每创建一个对象就躺count增加1
public Player() {
count ++;
System.out.println("一个玩家进入了游戏" + " 当前共有 " + count + " 名玩家");
}
}
输出:
4、调用方式
(1)在本类中的调用
在本类中可以直接调用,如:
(2)在其他类的调用
在其他类中需要使用 【类名】 + “.” + 【静态属性名】,或者使用【对象名】+ “.” + 【静态属性】
如:
5、在内存中的调用机制
假如我现在创建了一个player1和player2(简写p1p2),在我的笔记一中说过,在内存中是这样的:
当对象的类中有静态属性时,会在堆中产生一个存放静态变量的空间(至于具体的产生机制是类加载的内容,这个以后再说。。)
可见,当改变或者调用静态变量时,都是用的同一个区域的同一个变量,也就是被所有对象共享。
这里也要一提:在jdk8及以上的版本,静态变量的机制是这样的。而jdk8以前,静态变量在内存中是不同的。记住新版本的就可以了。
二、静态方法
1、定义
使用static修饰的方法就是静态方法
访问修饰符 static 数据返回类型 方法名(){}
(这里把static放在访问修饰符前边也可以)
2、调用方法
类名.静态方法名 或 对象名.静态方法名
当然 前提是满足访问权限的范围。
3、作用,使用方式
在不创建对象的时候就可以调用静态方法,比较方便,所以往往定义成工具类,来加速我们的程序开发。
举例:
public class MathUtils {
private MathUtils() {
// 私有构造函数,防止创建对象
}
/**
* 将两个整数相加
* @param a 第一个整数
* @param b 第二个整数
* @return 两个整数的和
*/
public static int add(int a, int b) {
return a + b;
}
/**
* 将第一个整数减去第二个整数
* @param a 被减数
* @param b 减数
* @return 两个整数的差
*/
public static int subtract(int a, int b) {
return a - b;
}
/**
* 将两个整数相乘
* @param a 第一个整数
* @param b 第二个整数
* @return 两个整数的乘积
*/
public static int multiply(int a, int b) {
return a * b;
}
/**
* 将第一个整数除以第二个整数
* @param a 被除数
* @param b 除数
* @return 两个整数的商
在主方法中调用,直接类.方法调用,十分便捷:
public class Main {
public static void main(String[] args) {
// 调用MathUtils工具类的add方法,将5和3相加
int sum = MathUtils.add(5, 3);
// 打印计算结果
System.out.println("Sum: " + sum);
// 调用MathUtils工具类的subtract方法,将8减去4
int difference = MathUtils.subtract(8, 4);
// 打印计算结果
System.out.println("Difference: " + difference);
// 调用MathUtils工具类的multiply方法,将2和6相乘
int product = MathUtils.multiply(2, 6);
// 打印计算结果
System.out.println("Product: " + product);
// 调用MathUtils工具类的divide方法,将10除以2
double quotient = MathUtils.divide(10, 2);
// 打印计算结果
System.out.println("Quotient: " + quotient);
}
}
4、使用细节
(1)静态方法不可以使用与对象有关的关键字如this super。
因为静态方法是与类直接关联的,不依赖于特定的对象实例,所以直接规定,不可使用this和super
(2)静态方法中只能访问静态方法或静态变量,普通变量不可出现在静态方法中。
(3)普通方法可以访问普通变量,也可以访问静态变量。
这点一定要与静态方法区分开
静态方法中只能访问静态变量。
普通方法可以访问普通变量,也可以访问静态变量。
(当然,也都需要符合访问权限的范围)
(4)静态方法不可以被重写
static 方法不可以被重写,因为它是属于类的,而不属于任何类的实例,而重写就是基于对象实例的。
三、main方法
我们重新认识一下main方法
1、main方法的定义
如图所示,main方法的访问修饰符是public的,同时也是static的,那么为什么Java设计者要这样定义主方法呢?
难道我们在不同包下还要访问main方法,所以才要把main方法定义为public的吗?显然不是
public class Main {
public static void main(String[] args) {
}
}
之所以main方法是public static的,是因为main方法是由Java虚拟机——也就是我们常说的jvm——来调用的。
而jvm调用main方法也就必然需要将main方法设置为public的才能访问。
值得一提的是,jvm并不属于任何包,但是jvm仍然可以调用带有public的方法。
而之所以使用的是static方法,是因为Java开发者希望jvm调用main方法时无需创建任何对象就可以调用main方法,所以设置为static。
2、main方法的参数列表
(String[] args)
这是main 方法的参数列表,可是我们并没有在使用main方法时传入这些参数不是吗?
如何使用main方法的参数?以及它们的作用是什么?
(1)使用main方法的参数
在文本编辑器中,执行完.javac编译以后,再使用java运行程序,可以在java 后添加一些String类型的参数,从而传递到main方法的参数列表中。
而在IDEA等开发工具中,我们往往不用手动编译,这样就不能再命令行中使用.java从而添加main方法的参数,但是我们可以在设置中设置这些参数,从而添加进去:
这样也可以传入进入,并可以在main方法中把这些参数打印出来:
public class CollectionHomework {
public static void main(String[] args) {
//使用增强for循环 遍历args
for (String s :args) {
System.out.println(s);
}
}
}
(2)main方法参数的作用
配置程序参数 调试和测试 批处理和自动化 程序配置和启动选项
这里我也举不了太具体的例子,放到后面说吧
四、代码块
1、什么是代码块?
代码块就是使用{}包裹起来的一段程序,它既没有方法名,也没有返回值,也不可以添加访问修饰符,但是可以添加static关键字。
代码块是类的成员之一,或者说类的一部分。只能在加载类的时候隐式的调用。
public class CodeBlock_ {
public static void main(String[] args) {
}
}
class Desk{
double price;
String name;
{
//代码块
}
public Desk(double price, String name) {
this.price = price;
this.name = name;
}
}
2、代码块的作用
代码块是构造器的一种补充机制,可以帮助构造器实现重复的工作。
具体是指,在构造器运行的时候,会首先运行代码块中的内容,然后再去执行构造器内部的代码。往往可以在代码块中执行一些初始化操作。
这里我添加了一些内容,来说明代码块的作用:
public class CodeBlock_ {
public static void main(String[] args) {
}
}
class Desk{
double price;
String name;
{
//代码块
}
public Desk(double price, String name) {
System.out.println("这是一张桌子,等待你的购买!");
this.price = price;
this.name = name;
}
public Desk(double price) {
System.out.println("这是一张桌子,等待你的购买!");
this.price = price;
}
public Desk(String name) {
System.out.println("这是一张桌子,等待你的购买!");
this.name = name;
}
}
这里有三个构造器,但是可以看到里面有一些重复的内容,如果我们一直重复定义,就非常消耗时间。
所以我们可以定义到代码块中,这样就简洁很多:
public class CodeBlock_ {
public static void main(String[] args) {
Desk desk = new Desk();
}
}
class Desk{
double price;
String name;
{
//普通代码块
System.out.println("这是一张桌子,等待你的购买!");
}
public Desk() {
}
public Desk(double price, String name) {
this.price = price;
this.name = name;
}
public Desk(double price) {
this.price = price;
}
public Desk(String name) {
this.name = name;
}
}
3、静态代码块
代码块前可以添加static关键字从而成为静态代码块:
static {
//静态代码块
}
在一个类中可以有普通代码块和静态代码块同时存在
静态代码块只能调用静态的成员,普通代码块则可调用任意成员。
静态代码块与普通代码块有什么区别呢?
普通代码块是构造器的补充,每创建一个新对象,就都会执行普通代码块中的代码。
而静态代码块与类的加载有关,当类加载时,就会执行静态代码块的内容。而类的加载只有一次,所以静态代码块也只会执行一次。
举例:如果是普通代码块,创建两个新对象,都会执行:
改为静态代码块则只会执行一次:
4、类什么时候会加载?(静态代码块调用机制)
有三种情况一个类会进行加载:
(1)创建对象示例时
(2)创建子类的对象示例时
(3)调用类的静态成员(方法、属性)时
总的来说,可以简要记为,只要第一次出现了与该类有关的代码时,该类就会加载。
5、创建对象时,类调用成员的顺序(重点!)
调用的顺序是
(1)首先进行静态属性的初始化和静态代码块的内容。
如果类中既有静态属性又有静态代码块,则按照代码的顺序依次执行。
(2)进行普通属性的初始化和普通代码块的内容。
如果都有,就按定义的顺序执行。
(3)调用构造器
public class CodeBlock_ {
public static void main(String[] args) {
Desk desk = new Desk();
}
}
class Desk{
//静态属性,为了便于看到初始化,这里调用静态方法来初始化
static String name = getName();
{
//普通代码块
System.out.println("普通代码块被调用");
}
static {
//静态代码块
System.out.println("静态代码块被调用");
}
//静态属性
static double price = getPrice();
public Desk() {
System.out.println("构造器被调用");
}
public static String getName() {
System.out.println("getName静态方法被调用");
return "desk";
}
public static double getPrice() {
System.out.println("getPrice静态方法被调用");
return 100.0;
}
}
运行结果:
如果继承了父类,这个子类的创建对象的调用顺序就更为复杂:
(1)父类的静态代码块和静态属性初始化
(2)子类的静态代码块和静态属性初始化
(3)父类的普通代码块和普通属性初始化
(4)父类的构造器
(5)子类的普通代码块和普通属性初始化
(6)子类的构造器
总结下来就是,如果有父类,就先调用父类的;如果有静态的就先调用父类静态和子类静态,然后就是代码块和构造器。
需要注意的是子类的静态代码块和静态属性初始化是在父类的普通代码块和普通属性初始化前执行的,这点容易错。
五、静态属性与方法的应用:单例模式
1、设计模式
在进行java项目开发时,可能会在开发过程中由于没有进行长久的规划,而出现程序的扩展性较差,如果想要增加新功能则需要大刀阔斧的改动的问题。
所以为了避免这种情况发生,我们需要按照一些特定的设计模式来开发程序,从而提高我们的效率。
常见的设计模式有:
当进行Java项目开发时,常见的设计模式有很多。以下是一些常见的设计模式:
(现在了解认识一下即可,以后都会学)
1. 单例模式(Singleton Pattern):确保一个类只有一个实例,并提供全局访问点。
2. 工厂模式(Factory Pattern):通过工厂类创建对象,隐藏具体实现细节,实现对象的创建和使用的分离。
3. 观察者模式(Observer Pattern):定义对象之间的一对多依赖关系,当一个对象状态发生变化时,其依赖的对象都会收到通知并自动更新。
4. 装饰器模式(Decorator Pattern):动态地给一个对象添加额外的功能,是继承关系的一种替代方案。
5. 策略模式(Strategy Pattern):定义一系列算法,将每个算法封装起来,并使它们可以互相替换,使得算法的变化独立于使用算法的客户端。
6. 适配器模式(Adapter Pattern):将一个类的接口转换成客户端所期望的另一个接口,使得原本不兼容的类可以合作。
7. 模板方法模式(Template Method Pattern):定义一个算法的骨架,将一些步骤延迟到子类中实现。
8. 建造者模式(Builder Pattern):将一个复杂对象的构建过程与其表示分离,使得同样的构建过程可以创建不同的表示。
9. 迭代器模式(Iterator Pattern):提供一种方法顺序访问一个聚合对象中的各个元素,而不需要暴露其内部表示。
10. 责任链模式(Chain of Responsibility Pattern):将请求的发送者和接收者解耦,使多个对象都有机会处理请求。
2、什么是单例模式?
所谓单例模式就是一个类只能存有一个实例对象,所以叫单例模式。
在之前的内容中,我们清楚的知道,当我们创建了一个类后,我们可以进一步创建多个这个类的对象,而单例模式中只允许存在一个该类的对象。
3、为什么要用单例模式(应用场景)?
在一个类只有一个对象时,可以避免资源重复创建和浪费。
当项目需要缓存对象来提高效率时,使用单例模式确保只有一个对象产生,可以避免对象重复创建和销毁。
等等等等……开发项目后大家会有更深刻的体会。
4、单例模式的两种实现方式
(1)饿汉式
例如一个人只能有一个女朋友,这样我们可以用单例模式实现。
步骤:
1、创建类时,让构造器私有化
这一步的作用就是让其他类不能直接通过new来创建对象。
2、在类的内部创建对象,从而让本类持有一个本类对象。
3、创建public static方法getInstance来返回本类的实例
这样就可以在其他类不能创建新对象的情况下来访问本类对象实例了。
由于static方法只能访问static成员,所以这里本类持有的实例也要设置为static类型。
public class SingleExcp {
public static void main(String[] args) {
//要在其他类中使用则必须使用getInstance方法
GirlFriend gf = GirlFriend.getInstance();
System.out.println("gf name = " + gf.getName());
}
}
//饿汉式
class GirlFriend {
//属性
private int age;
private String name;
//类的内部持有对象
private static GirlFriend gf = new GirlFriend(20, "mary");
//构造器私有化(防止在外部直接通过new创建对象)
private GirlFriend(int age, String name) {
this.age = age;
this.name = name;
}
//提供公共静态方法返回实例
public static GirlFriend getInstance(){
return gf;
}
//getter and setter
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
而为什么叫饿汉式,因为可以看到,即使我们没有使用GirlFriend类的实例对象,那么依然会在GirlFriend类创建一个对象,让其持有。所以可以见得这种模式非常”饥渴”,只要类加载了,即使没有使用对象,也会在类里创建对象,所以叫饿汉式。(真的)
而饿汉式的缺点也很明显了,就是不使用对象的情况下也要固定的创建一个对象,这样就会造成内存资源的浪费。
但是优点是饿汉式是线程安全的,可以在多线程的情况下运行。
(2)懒汉式
步骤:
1、创建类时,让构造器私有化
2、在类的内部创建对象的引用,在没有使用前为null。
3、创建public static方法getInstance来返回本类的实例,如果为null则创建对象,如果不为null则直接返回这个对象。
这样就可以让类中有且只有一个对象实例,并且实现了当要使用对象实例时,才创建这个对象实例。
public class SingleExcp {
public static void main(String[] args) {
//要在其他类中使用则必须使用getInstance方法
GirlFriend gf = GirlFriend.getInstance();
System.out.println("gf name = " + gf.getName());//gf name = mary
GirlFriend gf2 = GirlFriend.getInstance();
System.out.println("gf2 name = " + gf2.getName());//gf2 name = mary
//可见两个结果都是mary,这两个都是同一个对象
}
}
//懒汉式
class GirlFriend {
//属性
private int age;
private String name;
//这是对象实例的引用,但没有创建真正的对象,暂时为null
private static GirlFriend gf = null;
//构造器私有化(防止在外部直接通过new创建对象)
private GirlFriend(int age, String name) {
this.age = age;
this.name = name;
}
//提供公共静态方法返回实例,调用时创建对象
public static GirlFriend getInstance() {
//如果不存在实例(为null)就创建对象并返回
if (gf == null)
return new GirlFriend(20, "mary");
//存在对象就直接返回,不再重新创建
else return gf;
}
//getter and setter
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
懒汉式,可以看到,只有当使用到对象实例时,才会创建这个对象,避免了饿汉式的资源浪费。
但是同时也造成了线程不安全的问题,如两个线程同时想要使用对象实例时,可能会同时创建对象,造成各种问题。
六、final关键字
1、final的作用
使用final修饰属性,就可以让这个属性变为一个常量。
public class Final_ {
public static void main(String[] args) {
}
}
class FinalNum {
//圆周率pi就是一个常量 可以使用final修饰
final double PI = 3.1415;
}
当final关键字修饰一个方法时,这个方法不可以被重写
当final关键字修饰一个类时,这个类就不可以被继承。(只是不能继承,可以实例化)
2、final的使用细节
(1)使用final修饰的变量必须要初始化
初始化有三种方式:
1、在定义时初始化,就像在上面代码中写的一样
2、在构造器中初始化
3、在代码块中初始化
如果final修饰的变量没有初始化就会报错,不能运行。
(2)使用final修饰的变量不可以在后续代码中修改(常量不可修改)
举例:
注意,这里的不能修改是指后续代码,但是final修饰的变量的初始化的值还是可以修改的。
(3)final往往和static关键字配合使用
final static类型的属性,是一个常量,但是在jvm调用的时候效率相比final属性更高,所以推荐将final和static关键字配合使用。
(4)final static属性的初始化的范围
final static常量显然也是需要初始化的。
而其初始化的范围有所变化:
1、定义时初始化
2、静态代码块中初始化(构造器中不可以初始化)
(5)final属性命名规范
往往使用大写字母并使用"_"隔开
例如:
class FinalNum {
//单位圆的面积
final static double AREA_OF_UNIT_CIRCLE = 3.14;
}
(6)如果一个类时final的,那么就不用把其内部的方法再修饰为final
原因是显然的,不可被继承的类的方法,自然不会被重写。
(7)final不可以修饰构造器
七、抽象类
1、什么是抽象类?
有时候,我们定义一个方法时,只知道一些大概的情况,但是并不知道到底如何去实现,这时候我们就可以将这个方法定义为抽象方法。
使用abstract关键字修饰一个方法,这个方法就会变为抽象方法。
而拥有抽象方法的类,就必须也使用abstract修饰为抽象类。
举例:
abstract class Animal {
//定义一个动物移动的方法,但是这个方法还并不确定
//也就是不知道怎样实现 修饰为抽象方法
public abstract void move();
}
之后,当一个子类继承抽象父类的时候,子类就必须要具体实现这个抽象父类的抽象方法,这样再去具体实现方法,就可以让方法更加规范,也提高了方法的可维护性。
abstract class Animal {
//定义一个动物移动的方法,但是这个方法还并不确定
//也就是不知道怎样实现 修饰为抽象方法
public abstract void move();
}
class Horse extends Animal {
//子类重写父类抽象方法
@Override
public void move() {
System.out.println("马在奔跑~");
}
}
class Lion extends Animal {
@Override
public void move() {
System.out.println("狮子也在跑。。");
}
}
为什么说更规范呢?
可以看到,马类和狮子类都应该有奔跑的方法,那么马类和狮子类的这个奔跑方法名应该谁来规范呢?
不规范的方法名,这样的程序调用起来就会降低程序编写的效率,这对我们的开发而言是不利的。
所以使用抽象类,就可以有效规范方法。
也正因如此,抽象类在设计模式以及程序框架中使用的非常多(面试也常问)。
2、抽象类使用细节
(1)抽象方法不可以有方法体,要加分号。
(2)抽象类不能实例化
很好理解,类都是抽象的了,又怎么能实例呢?
(3)一个类中有抽象方法,就要把类修饰为抽象类
这是一个规范,记住并遵守就行。
(4)abstract只能修饰类和方法,不能修饰别的。
(5)抽象类可以有非抽象的普通方法,和其他的普通属性等内容
见代码:
abstract class Animal {
//定义一个动物移动的方法,但是这个方法还并不确定
//也就是不知道怎样实现 修饰为抽象方法
public abstract void move();
//跟普通类一样,也可以有普通属性、普通方法、构造器等
int age;
String name;
public void sayHi(){
System.out.println("hi!");
}
public Animal(int age, String name) {
this.age = age;
this.name = name;
}
}
(6)抽象类中可以没有抽象方法,但是一旦一个类有抽象方法,就一定要修饰为抽象类。
(7)一旦一个类继承了一个抽象类,就要实现抽象类的所有抽象方法,或者把自己也修饰为抽象类
举例:
实现方法后就不报错了。
注意,可以不实现抽象类中的非抽象方法。
或者自己也是一个抽象类也不报错:
(8)抽象方法不可以使用static、private 和final 来修饰
这些关键字都是和重写矛盾的,所以不可以使用在抽象方法中。
static:static 方法不可以被重写,因为它是属于类的,而不属于任何类的实例,而重写就是基于对象实例的,所以不可使用static修饰抽象方法。
private:访问权限就在本类,自然不可以被子类重写
final:修饰的本身就是不可重写的意思
3、抽象类的应用场景
当一个功能内部一部分是确定的,一部分是不确定的时,就可以使用抽象类。
举例:现在我想要计算某些任务的完成耗时:
这里我定义一个TimeCounter抽象类
里边有普通方法countTime和抽象方法job
这里的job就是我想要测量耗时的任务,但是这个任务具体是什么,我还不知道。
之后,让A和B类继承抽象类TimeCounter,这样job方法就可以由A类和B类实现。
然后,我就可以调用TimeCounter这个抽象类中的普通方法,countTime来进行job方法用时的计算。
而至于job方法由于已经被A类和B类重写过了,所以在A类调用的时候,countTime方法就会通过动态绑定机制来绑定到A类重写的job方法,在B类调用的时候,countTime方法就会通过动态绑定机制来绑定到B类重写的job方法。
这样一来,不管是A类还是B类,都可以通过这个抽象类来计算方法的用时。而不用在本类重新定义一些计时的方法。
八、接口
注意区分接口和抽象类的区别,他们十分相似
1、什么是接口?
Java程序之间为了定义某种统一的规范,可以使用接口来加强程序之间的规范性。
这里我定义一个简单的接口:
public interface Interface_ {
//接口
public void Usb();
public void HDMI();
}
接口里有两个方法,Usb、HDMI,两个方法都没有方法体。
我们可以让一个类实现一个接口,使用implement关键字:
这时,我们就需要去实现接口中的方法,不然就会像上图那样报错。
class Computer implements Interface_{
@Override
public void Usb() {
System.out.println("使用usb接口");
}
@Override
public void HDMI() {
System.out.println("使用HDMI接口");
}
}
是不是跟抽象类很相似,后面我们会提到它们的区别。
2、为什么要有接口
现在设想一个场景:
有三个程序员想要访问mysql数据库,他们分别编写了三个类,来访问mysql数据库。
程序员A:
public class AA {
//程序员A写的
public void f1() {
System.out.println("访问数据库的方法");
System.out.println("对数据库操作A");
}
public void f2() {
System.out.println("与数据库断开的方法");
}
}
程序员B:
public class BB {
//B程序员写的
public void connect(){
System.out.println("连接数据库的方法");
System.out.println("对数据库操作B");
}
public void close(){
System.out.println("与数据库断开的方法");
}
}
程序员C:
public class CC {
//程序员C写的
public void getConnection(){
System.out.println("连接数据库的方法");
System.out.println("对数据库操作C");
}
public void shutdown(){
System.out.println("与数据库断开的方法");
}
}
可以见得,这三个程序员写的方法虽然都是连接数据库和与数据库断开,但是他们的方法名并不相同,这样就会造成混乱,不同程序员间看代码会造成混淆,降低效率。
但是如果项目经理提前写好一个接口,让三个程序员实现,那么就可以统一方法名。
3、接口使用细节
(1)jdk7版本中,接口里的方法都不可以有方法体
如图:
(2)jdk8以后,接口的方法可以是静态方法,并且可以有方法体
当方法前使用default关键字修饰时,可以有方法的具体实现。
public interface MyInterface {
//使用default关键字
default void defaultMethod() {
System.out.println("这是一个 default 方法.");
}
}
(3)接口不可以被实例化
即不能new一个接口对象
(4)接口中的所有方法都是public的,如果缺省(default关键字)则默认是abstract方法
这就解释了为什么默认的接口的方法不能有方法体。
(5)普通类实现接口后,必须将所有的接口中的方法都重写(有default修饰的则不用)
原因也是默认方法都是abstract方法,所以必须重写。
如果使用default关键字,则可以不用重写。
(6)抽象类实现接口后,可以不用重写接口中的方法
跟之前抽象类和抽象方法的内容很像。
(7)一个类可以实现多个接口(与继承抽象类不同)
Java是单继承机制,只能继承一个类,但是一个类却可以实现多个接口。
举例:使用 “,” 间隔不同的接口。
(8)接口中的属性只能是public static final(默认也是public static final的)需要初始化
示例:
public interface Interface_01 {
//接口中定义属性
int a = 1;
}
public class Interface02 {
public static void main(String[] args) {
DDD ddd = new DDD();
System.out.println(ddd.b);
}
}
class DDD implements Interface_01 {
//DDD实现接口 可以使用a属性,并且没有创建对象,说明是public static的
int b = a + 1;
}
如果修改a的值,则会报错,因为默认是final属性,不可在后续代码修改值:
因为接口不能创建新对象,所以想要直接访问接口中的属性,就只能是static属性。
综上,接口中的属性总是public static final 的,并且必须初始化。
(9)实现了接口的类的属性,可以与接口中的属性重名
重名后,就需要使用接口名+.+属性名来访问接口中的属性。可以看下面的示例:
(10)接口中的属性的访问形式
【接口名】+ “.” +【属性名】
跟类的静态变量相似,类是类名 + . + 属性名,接口则是接口名
a是DDD类的属性与接口中的a重名了,如果想访问接口中的a则需要用【接口名】+ “.” +【属性名】,不写则默认是DDD类中的a。
(10)接口可以使用 extends 关键字继承其他的接口。
接口继承接口以后,一个类如果继承了子接口,那么父接口的方法也就要实现。
因为子接口继承父接口以后,子接口就会重写父接口的抽象方法,而类继承子接口后,类就要实现子接口的抽象方法,自然也就实现了其父接口的方法。
(11)修饰接口的访问修饰符只能够用public和默认
4、实现接口与继承类的不同
实现接口需要满足xxx 像 xxx,只要需要功能,就可以实现接口。
继承类需要满足 xxx 是 xxx ,需要满足一个东西是一个东西才可以。
接口的作用是制定各种规范,从而让代码更加灵活。
继承的作用则是让代码的复用性和可维护性更好。
总结:接口更多地被用于定义一个可以由多个不同类实现的通用契约,而继承则用于表达更具体的“是一个”关系,并实现代码复用。
5、接口的多态
(1)当一个方法的参数列表设置为一个接口,那么这个参数就可以接受实现了这个接口的方法。
接口作为参数传入方法举例:
public class Interface02 {
public static void main(String[] args) {
returnName(new DDD());
returnName(new EEE());
}
//编写方法体现接口的多态:
public static void returnName(Interface_01 itf){
System.out.println(itf);
}
}
//DDD类实现了Interface_01接口
class DDD implements Interface_01 {
String name = "DDD";
//重写toString方法
@Override
public String toString() {
return "DDD{" +
"name='" + name + '\'' +
'}';
}
}
//EEE类实现了Interface_01接口
class EEE implements Interface_01 {
String name = "EEE";
//重写toString方法
@Override
public String toString() {
return "EEE{" +
"name='" + name + '\'' +
'}';
}
}
输出结果:
(2)接口可以作为多态数组
举例:
public class Interface02 {
public static void main(String[] args) {
Interface_01[] ifts = new Interface_01[2];
ifts[0] = new DDD();
ifts[1] = new EEE();
//可以看到,接口多态数组,可以传入实现了接口的类对象
}
}
//DDD类实现了Interface_01接口
class DDD implements Interface_01 {
}
//EEE类实现了Interface_01接口
class EEE implements Interface_01 {
}
(3) 接口可以作为对象的编译类型出现
见后面的匿名内部类
九、内部类(inner class)
内部类在Java体系中非常重要,如果没有掌握熟练,对后续学习会有障碍
1、什么是内部类
在我们创建一个类后,可以在类的内部再创建一个类,叫做内部类。
内部类共有四种形式:
(1)局部内部类
(2)匿名内部类
(3)成员内部类
(4)静态内部类
我们来分别讲解一下
2、局部内部类
(1)局部内部类有类名,定义在外部类的局部位置,如:方法中。
(2)局部内部类的地位是一个局部变量,不可以用访问修饰符修饰。但可以使用final修饰。
(3)局部内部类的作用域仅仅是在定义它的方法体或代码块中。
(4)局部内部类的本质还是一个类。
(5)局部内部类可以直接访问外部类的成员。
(6)外部类访问局部内部类的成员,需要创建对象才能访问。(并且在其作用域中)
用代码说明一下:
public class InnerClass_ {
public static void main(String[] args) {
}
}
//外部类
class OuterClass{
//外部类的属性
int age = 10;
String name;
//外部类的方法
public void Method01(){
//方法中的局部变量
int c = 1;
String str = "";
//局部内部类
class InnerClass {
//局部内部类可以直接访问外部类的成员age
public int a = age + c;
//局部内部类本质上还是类,所以也可以有构造器
public InnerClass() {
}
public InnerClass(int a) {
this.a = a;
}
//局部内部类本质上还是类,所以也可以有方法
public int getA() {
return a;
}
}
//外部类访问局部内部类的属性 (注意,需要在局部内部类定义的后面再访问)
//并且需要在局部内部类的作用域之内
InnerClass innerClass = new InnerClass();
System.out.println(innerClass.a);
}
}
如果外部类想要在局部内部类定义之前访问局部内部类,就无法访问。
如:
(7)如果外部类的成员和局部内部类的成员重名,则按以下规则访问:
1、外部类访问外部类重名成员,局部内部类访问局部内部类重名成员,都按照就近原则进行。
2、外部类访问局部内部类重名成员,通过局部内部类对象访问。
3、局部内部类访问外部类重名成员,通过【外部类名】+ “.” + 【this】+ “.” + 【成员名】调用。
代码示例:
public class InnerClass_ {
//主方法
public static void main(String[] args) {
OuterClass outerClass = new OuterClass();
outerClass.Method01();
}
}
//外部类
class OuterClass {
//外部类的属性a,与内部类属性重名
int a = 11;
public void Method01() {
//内部类
class Innerclass01 {
//内部类属性a与外部类属性重名
int a = 55;
//访问外部类的a
public int OuterA() {
return OuterClass.this.a;
}
}
//外部类访问内部类的a
Innerclass01 innerclass01 = new Innerclass01();
System.out.println(innerclass01.a);
//通过内部类的方法 打印外部类的a
System.out.println(innerclass01.OuterA());
}
}
运行结果:
3、匿名内部类(重点!)
匿名内部类没有类名,或者说类名是系统给出的并隐藏起来的。
它的作用之一是,当我们需要一次性的用一个类时,作为一个临时的类,用完就销毁。
它的用法更多的是作为一个参数传递给方法。
往往需要搭配接口使用。
语法是这样的:
new 类/接口 (参数列表){ 类体 };
注意,这里的类/接口,不是匿名内部类的类名。后面看例子就知道了。
(1)使用场景一:需要一次性使用某类时
现在设想我们需要用到某类,但是只需要用到一次,用完之后就没用了。这是我们可以考虑使用匿名内部类。
例如:
public class InnerClass2_ {
public static void main(String[] args) {
OuterClass02 outerClass02 = new OuterClass02();
outerClass02.Method1();
}
}
//接口
public interface IA {
//接口定义了一个方法
public void drive();
}
class OuterClass02 {
public void Method1(){
//匿名内部类 使用接口来创建匿名内部类
IA car = new IA(){
//实现接口的方法
@Override
public void drive() {
System.out.println("汽车在行驶~");
}
};
//使用方法
car.drive();
}
}
我们使用匿名内部类重写了接口中的drive方法,从而让car来一次性的使用,使用之后,这个匿名内部类就会销毁。
从上面的代码也可以看到,匿名内部类的创建是比较古怪的,在new IA()后直接就写了一个类的类体{},并直接重写了接口的方法。
那么这里有一个问题:
可以看到car的编译类型是接口IA,但是car的运行类型是什么呢?(编译类型与运行类型)
答案就是这个匿名内部类。
我们使用getclass方法来看一看:
可以看到,car的运行类型是 OuterClass02$1,这个就是我们匿名内部类隐藏在系统中的“名字”。这是系统自动分配的,是外部名+$数字 构成的。数字就是第几个匿名内部类
(2)使用场景二:作为参数传入方法中
这个在我们java的各种框架中运用的很多,要多理解。
我们知道创建一个类,就是创建了一个数据类型。所以我们可以把类来作为方法的参数。
而接口也是一样的,也可以出现在方法的参数列表中,见上面接口的多态。
而我们如果为了传入参数,就创建一个类并实现接口的方法,可能就会造成代码的冗余和资源的浪费。所以我们可以使用匿名内部类传入方法中。
public class InnerClass3_ {
public static void main(String[] args) {
//创建外部类对象
OuterClass03 outerClass03 = new OuterClass03();
//使用外部类的方法 创建匿名内部类 并重写IA接口的drive方法
outerClass03.Method(new IA() {
@Override
public void drive() {
System.out.println("匿名内部类重写的drive~");
}
});
}
}
class OuterClass03 {
//外部类的方法 参数时IA接口 即需要传入一个实现了IA接口的类对象
public void Method(IA ia) {
//调用IA接口传入对象的drive方法
ia.drive();
}
}
运行结果:
4、成员内部类
(1)使用与细节
成员内部类在类的成员位置,他的地位和类的成员一样。
成员内部类可以直接访问外部类的其他成员
例如:
//外部类
class Phone {
//外部类的属性
int price;
String color;
//成员内部类 本质上还是类,可以有属性 方法 构造器等
class chips {
//成员内部类的属性
int chipPrice;
//成员内部类的方法
public void showChipPrice(){
System.out.println(chipPrice);
}
//可以直接调用外部类的属性
public void showColor(){
System.out.println(color);
}
//构造器
public chips(int chipPrice) {
this.chipPrice = chipPrice;
}
}
}
成员内部类还可以添加任意访问修饰符,因为其本质就是一个成员。
外部类访问内部类成员,需要先创建对象再访问
成员内部类的作用域为整个类体。
外部其他类访问成员内部类,则需要使用特殊的语法,先创建成员内部类对象,再访问。
例如:
Phone phone = new Phone(8000, "black");
//创建内部类对象
Phone.chips chips = phone.new chips(1000);
也可以在外部类中编写一个方法来返回内部类对象。(这里不展示了)
如果外部类的成员和内部类重名了,内部类访问优先访问内部类的,外部类访问优先访问外部类的。
外部类访问内部类则创建对象访问。
public void AAA(){
System.out.println(new chips(100).chipPrice);
}
内部类访问外部类,使用【外部类名】+ “.” + 【this】+ “.” + 【成员名】来访问。(跟局部内部类一样)
(2)应用场景
当某个类只在另一个类中使用时:如果一个类仅仅被另一个类使用,那么将这个类定义为成员内部类可以更好地封装类之间的关系。
实现多重继承:Java不支持多重继承,但可以通过内部类实现类似的效果。一个类可以通过包含一个或多个内部类来继承多个类或实现多个接口。
逻辑上的分组:当两个类在逻辑上属于同一组时,可以将一个类定义为另一个类的成员内部类,这样可以更加清晰地组织代码,提高可读性。
增加封装性:成员内部类可以访问外部类的私有数据成员,这样可以在不对外暴露私有数据的情况下,增加类的封装性和安全性。
5、静态内部类
使用方式与细节
静态内部类说白了就是使用static关键字修饰的成员内部类。
而既然是静态的,就会有静态的相关特性:
可以直接访问外部类的所有静态成员,但不可以访问非静态成员
可以添加访问修饰符
外部其他类访问静态内部类,可以先创建静态内部类对象后访问。
如:
public class InnerClass05_ {
public static void main(String[] args) {
//创建静态内部类对象后
TTT.RRR rrr = new TTT.RRR();
//直接访问
rrr.say();
}
}
class TTT{
//静态内部类
static class RRR{
public void say(){
System.out.println("hi!");
}
}
}
出现重名的属性和成员内部类一样,除了内部类访问外部类对象时,
使用【外部类.属性】,不用加this。
终于写完了,共整理了近两万字,希望大家多多支持!
后续还会更新新的笔记,下次来整理枚举、异常、常用类和集合的内容,可以关注我看之前的笔记,和后续更新!