目录
一、类变量(静态变量)
(一)问题引入
现在有一个需求:有一群孩子在玩耍,不断有新的孩子加入,请问如何知道现在共有多少孩子在玩?按照以往的思路,代码实现如下:
public class ChildGame {
public static void main(String[] args) {
int count = 0;
Child child1 = new Child("张三");
child1.join();
count++;
Child child2 = new Child("李四");
child2.join();
count++;
Child child3 = new Child("王五");
child3.join();
count++;
System.out.println("共有" + count + "个人加入了游戏");
}
}
class Child {
private String name;
public Child(String name) {
this.name = name;
}
public void join() {
System.out.println(name + "加入了游戏...");
}
}
运行结果:
从代码中我么可以看出,count这个变量是一个独立于对象的变量,外界可能无法访问,没有使用到OOP。因此,针对这个问题,可以用类变量/静态变量来解决、
(二)类变量快速入门
思考:如果设计一个int count表示总人数,在创建一个对象时,就把count加1,并且count是所有对象共享的就ok了。使用类变量解决上述问题如下:
public class ChildGame {
public static void main(String[] args) {
// int count = 0;
Child child1 = new Child("张三");
child1.join();
// count++;
child1.count++;
Child child2 = new Child("李四");
child2.join();
// count++;
child2.count++;
Child child3 = new Child("王五");
child3.join();
// count++;
child3.count++;
// 类变量可以通过类名来访问
System.out.println("共有" + Child.count + "个人加入了游戏");
System.out.println("child1.count==" + child1.count);
System.out.println("child2.count==" + child2.count);
System.out.println("child3.count==" + child3.count);
}
}
class Child {
// 定义一个变量count,该变量是一个类变量(静态变量)
// static修饰的就是静态的
public static int count = 0;
private String name;
public Child(String name) {
this.name = name;
}
public void join() {
System.out.println(name + "加入了游戏...");
}
}
(三)类变量的内存布局
static修饰的变量成为类变量/静态变量,该变量在不同的JDK版本中,存放的位置不同:
- Java 6及之前的版本:JVM使用了方法区来存储静态变量。
- Java 8及之后的版本:静态变量被存储在堆中。
但是不管static变量在哪里,我们需要记住的是:
- static变量是同一个类中所有对象共享
- static变量在类加载的时候就生成了
(四)类变量的定义与访问
1.类变量定义
类变量也叫静态变量/静态属性,是该类的所有对象共享的变量,任何一个该类的对象去访问它时,取到的都是相同的值,同样任何一个该类的对象去修改它时,修改的也是同一个变量。
2.类变量的定义语法
- 访问修饰符 static 数据类型 变量名; [推荐]
- static 访问修饰符 数据类型 变量名; [不推荐]
3.访问类变量
- 类名.类变量名 [推荐]
- 或者 对象名.类变量名 [不推荐]
- 注意:静态变量的访问修饰符的访问权限和范围 和 普通属性时一样的!
public class VisitStatic {
public static void main(String[] args) {
// 类变量是随着类的加载而创建的,所以即使没有创建对象实例也可以访问
System.out.println(A.name); // 张三
A a = new A();
System.out.println(a.name); // 张三
}
}
class A {
public static String name = "张三";
// private修饰的类变量外界无法访问
// private static String name = "张三";
}
(五)类变量使用注意事项和细节
- 什么时候需要使用类变量:当我们需要让某个类的所有对象都共享同一个变量时,就可以考虑使用类变量(静态变量):比如:定义学生类,统计所有学生共交多少钱。Student(name,static double fee)
- 类变量与实例变量(普通属性)的区别:类变量是该类的所有对象共享的,而普通属性是每个对象独享的。
- 加上static的变量称为类变量或静态变量,否则称为实例变量/普通变量/非静态变量
- 类变量可以通过 类名.类变量名 或者 对象名.类变量名 来访问,但是推荐使用 类名.类变量名。[前提是:满足访问修饰符的访问权限和范围]
- 实例变量不能通过 类名.类变量名 方式访问。
- 类变量是在类加载时就初始化了,也就是说,即使你没有创建对象,只要类加载了,就可以使用类变量了。
- 类变量的生命周期是随类的加载开始,随着类消亡而销毁。
二、类方法(静态方法)
(一)类方法基本介绍
类方法也叫静态方法,形式如下:
- 访问修饰符 static 数据返回类型 方法名(){} [推荐]
- static 访问修饰符 数据返回类型 方法名(){}
类方法的调用:类名.方法名 注意要满足访问修饰符的访问权限和范围
类方法调用代码示例:
public class StaticDetail {
public static void main(String[] args) {
Stu tom = new Stu("tom");
Stu.payFee(100);
Stu marry = new Stu("marry");
Stu.payFee(200);
Stu.showFee(); // 总学费:300.0
}
}
class Stu {
private String name;
private static double fee;
public Stu(String name) {
this.name = name;
}
public static void payFee(double fee) {
Stu.fee += fee;
}
public static void showFee() {
System.out.println("总学费:" + Stu.fee);
}
}
(二)类方法使用场景
当方法中不涉及到任何和对象相关的成员,则可以将方法设计成静态方法,提高开发效率。比如:工具类中的方法utils:Math类、Arrays类、Collections集合类等。
开发过程中,如果我们希望不创建对象实例,也可以调用某个方法(即当做工具来使用),这时,把方法做成静态方法是非常合适的。,比如打印一堆数组,冒泡排序,完成某个计算任务等。
(三)注意事项和使用细节
- 类方法和普通方法,都是随着类的加载而加载,将结构信息存储在方法区,类方法中不能使用this和super关键字,普通方法中可以;
- 类方法可以通过类名调用,也可以通过对象名调用
- 普通方法和对象有关,需要通过对象名调用(对象名.方法名(参数)),不能通过类名调用。
- 类方法(静态方法)中只能访问静态变量或静态方法
- 普通成员方法,既可以访问普通成员(属性/方法),也可以访问静态成员(静态成员属性和静态成员方法)。如下图。
小结:
- 静态方法,只能访问静态成员;
- 非静态方法,可以访问所有成员;
- 在编写代码时,仍然要遵守访问权限规则。
小练习:判断下面代码的输出结果:
public class StaticMethodDetail {
public static void main(String[] args) {
new Test().count(); // count=9
new Test().count(); // count=10
System.out.println(Test.count); // 11
}
}
class Test {
static int count = 9;
public void count() {
System.out.println("count=" + (count++));
}
}
三、理解main方法语法
解释main方法的形式:public static void main(String[] args){}
- main方法是由虚拟机调用的
- Java虚拟机需要调用类的main()方法,所以该方法的访问权限必须是public
- Java虚拟机在执行main()方法时,不必创建对象,所以该方法必须是static
- 该方法接收String类型的数组参数,该数组中保存执行Java命令时传递给所运行的类的参数
- 命令行形式: java 执行的程序 参数1 参数2 参数3
- 在IDEA中动态传值:
特别提示:
- 在main()方法中,可以直接调用main方法所在类的静态成员(静态变量和静态方法)
- 但是,不能直接访问该类中的非静态成员,必须创建该类的一个实例对象后,才能通过这个对象去访问类中的非静态成员。
四、代码块
(一)基本介绍
代码块又称为初始化块,属于类中的成员(即 是类的一部分),类似于方法,在方法体中,通将逻辑语句封装过{}包围起来。
但和方法不同,代码块没有方法名,没有返回,没有参数,只有方法体,而且不用通过对象或类显式调用,而是在加载类时,或创建对象时隐式调用。
(二)基本语法
[修饰符]{
代码
};
注意:
- 修饰符可选,要么不写,要么只能写static
- 代码块分为两类,使用static修饰的叫静态代码块,没有static修饰的,叫普通代码块
- 代码块中可以写任何逻辑语句(输入、输出、方法调用、循环、判断等)
- ;号可以写,也可以不写
(三)代码块的作用
- 相当于另外一种形式的构造器(对构造器的补充机制),可以做初始化的操作。
- 如果多个构造器中都有重复的语句,可以将构造器中重复的语句,抽取到代码块中,代码块会先于构造器加载,并且每创建一个对象,都会先加载代码块的内容,然后加载构造器的内容。
- 代码块的调用顺序优先于构造器。
public class CodeBlock01 {
public static void main(String[] args) {
Movie movie1 = new Movie("灰姑娘");
System.out.println("------------------------");
Movie movie2 = new Movie("泰坦尼克号", 55);
System.out.println("------------------------");
Movie movie3 = new Movie("蝙蝠侠", 75, "蒂姆·伯顿");
}
}
class Movie {
private String name;
private double price;
private String director;
{
System.out.println("电影屏幕打开...");
System.out.println("广告开始...");
System.out.println("电影正式开始...");
}
public Movie(String name) {
System.out.println("Movie(String name)被调用");
this.name = name;
}
public Movie(String name, double price) {
System.out.println("Movie(String name, double price)被调用");
this.name = name;
this.price = price;
}
public Movie(String name, double price, String director) {
System.out.println("Movie(String name, double price, String director)被调用");
this.name = name;
this.price = price;
this.director = director;
}
}
运行结果:
(四)注意事项与使用细节
1.static代码块也叫静态代码块,作用就是对类进行初始化,而且它随着类的加载而执行,并且只会执行一次。
如果是普通代码块,每创建一个对象,就执行一次。
2.类在什么时候会被加载
(1)创建对象实例时(new) 见图一
(2)创建子类对象实例,先加载父类,后加载子类 见图二
(3)使用类的静态成员时(静态属性,静态方法) 见图三
图一:创建对象实例时会加载类
图二:创建子类对象时,会先加载父类,后加载子类
图三:试用类的静态成员时,先加载静态代码块,然后按照调用顺序分别加载静态属性和静态方法。
图四:当父子类都有静态代码块时,先加载父类的静态代码块,然后加载子类的静态代码块,并且只会执行一次,最后按照main方法中的调用顺序加载。
3.普通的代码块,在创建对象实例时,会被隐式调用:对象被创建一次,就会调用一次。
如果只是使用类的静态成员时,普通代码块并不会执行。
图六:创建对象时,最先加载一次静态代码块,然后创建几次对象,就加载几次普通代码块。
图七:类加载的时候,不会执行普通代码块
小结:
(1)静态代码块在类加载的时候执行,且只执行一次;
(2)普通代码块在创建对象时调用的,创建一次,加载一次;
(3)类加载的3种情况
4.创建一个对象时,在一个类中的调用顺序:
(1)调用静态代码块和静态属性初始化
注意:静态代码块和静态属性初始化调用的优先级一样,如果有多个静态代码块和多个静态变量初始化,则按他们定义的顺序调用。
下面的代码中,静态成员按照编写顺序执行,但是代码中最先调用的是getN1()方法,所以getN1()会先执行,再执行静态代码块:
如果将静态代码块放在最前面,那么就先执行静态代码块中的内容:
(2)调用普通代码块和普通属性的初始化
注意:普通代码块和普通属性初始化调用的优先级一样,如果有多个普通代码块和多个普通属性初始化,则按定义顺序调用。
(3)调用构造方法
public class CodeBlockDetail02 {
public static void main(String[] args) {
new A();
System.out.println("------------------------");
new A(8);
}
}
class A {
{
System.out.println("普通代码块被调用...");
}
private static int n1 = getN1();
private int n2 = getN2();
static {
System.out.println("A 静态代码块被调用...");
}
public static int getN1() {
System.out.println("getN1()被调用...");
return 100;
}
public int getN2() {
System.out.println("getN2()被调用...");
return 200;
}
public A() {
System.out.println("执行无参构造器...");
}
public A(int n2) {
System.out.println("执行有参构造器...");
this.n2 = n2;
}
}
5.构造器的最前面其实隐含了super()和调用普通代码块,静态相关的代码块,属性初始化,在类加载时,就执行完毕。因此是优先于构造器和普通代码块执行的。
6.创建一个子类对象时(有继承关系),他们的静态代码块、静态属性初始化、普通代码块、普通属性初始化、构造方法的调用顺序如下:
(1)父类的静态代码块和静态属性(优先级一样,按定义顺序执行)
(2)子类的静态代码块和静态属性(优先级一样,按定义顺序执行)
(3)父类的普通代码块和普通属性初始化(优先级一样,按定义顺序执行)
(4)父类的构造方法
(5)子类的普通代码块和普通属性初始化(优先级一样,按定义顺序执行)
(6)子类的构造方法
public class CodeBlockDetail04 { public static void main(String[] args) { //老师说明 //(1) 进行类的加载 //1.1 先加载 父类 A02 1.2 再加载 B02 //(2) 创建对象 //2.1 从子类的构造器开始 new B02();//对象 // new C02(); } } class A02 { //父类 private static int n1 = getVal01(); static { System.out.println("A02的一个静态代码块..");//(2) } { System.out.println("A02的第一个普通代码块..");//(5) } public int n3 = getVal02();//普通属性的初始化 public static int getVal01() { System.out.println("getVal01");//(1) return 10; } public int getVal02() { System.out.println("getVal02");//(6) return 10; } public A02() {//构造器 //隐藏 //super() //普通代码和普通属性的初始化...... System.out.println("A02的构造器");//(7) } } class B02 extends A02 { // private static int n3 = getVal03(); static { System.out.println("B02的一个静态代码块..");//(4) } public int n5 = getVal04(); { System.out.println("B02的第一个普通代码块..");//(9) } public static int getVal03() { System.out.println("getVal03");//(3) return 10; } public int getVal04() { System.out.println("getVal04");//(8) return 10; } public B02() {//构造器 //隐藏了 //super() //普通代码块和普通属性的初始化... System.out.println("B02的构造器");//(10) // TODO Auto-generated constructor stub } }
7.静态代码块只能调用静态成员(静态属性和静态方法),普通代码块可以调用任意成员。
class C02 { private int n1 = 100; private static int n2 = 200; private void m1() {} private static void m2() {} static { //静态代码块,只能调用静态成员 //System.out.println(n1);错误 System.out.println(n2); //m1();//错误 m2(); } { //普通代码块,可以使用任意成员 System.out.println(n1); System.out.println(n2); m1(); m2(); } }
(五)练习题
1.题目1
2.题目2
五、static在设计模式的经典应用——单例模式
设计模式:在大量的实践中总结和理论化之后优选的代码结构、编程风格、以及解决问题的思考方式。
单例模式:所谓的单例设计模式,就是采取一定的方法,保证在整个的软件系统中,对某个类只能存在一个对象实例,并且该类只提供一个取得其对象实例的方法。比如我们开发中有一个类是重量级的,非常耗用资源,但是实际上我们只需要一个对象,就可以使用单例设计模式。
简言之,就是只能通过一个方法,创建唯一一个对象。
(一)饿汉式
为什么叫“饿汉式”:不管你是否需要,先提供一个(很着急~)
饿汉式的弊端:可能创建了一个对象,但是没有使用。
饿汉式实现步骤:
- 为了防止在外面传参new多个对象,将构造器私有化
- 在类的内部直接创建私有的静态对象(注意:这里的创建对象私有化必须要有一个static修饰,否则下面的公共静态方法无法调用!!!)
- 提供一个公共的静态方法返回这个对象
代码示例:
public class SingleTon01 {
public static void main(String[] args) {
// 原来可以new多个对象
// GirlFriend aa = new GirlFriend("aa");
// GirlFriend bb = new GirlFriend("bb");
// 直接通过方法可以获取对象
Cat cat = Cat.getInstance();
System.out.println(cat);
Cat cat1 = Cat.getInstance();
System.out.println(cat1);
// 两个对象是同一个对象,即,无论调用多少次,都只有一个对象
// 因为静态调用的只会在内存中加载一次,这样就保证了不会有更多的对象
System.out.println(cat == cat1); // true
}
}
class Cat {
private String name;
// 1.为了防止在外面传参new多个对象,将构造器私有化
private Cat(String name) {
this.name = name;
}
// 2.在类的内部直接创建私有的静态对象
// 注意:这里的创建对象私有化必须要有一个static修饰,否则下面的公共静态方法无法调用!!!
private static Cat cat = new Cat("cc");
// 3.提供一个公共的静态方法返回这个对象
// 注意:如果下面的getInstance()方法不用static修饰,外部就可以创建Cat对象了
public static Cat getInstance() {
return cat;
}
@Override
public String toString() {
return "Gat{" +
"name='" + name + '\'' +
'}';
}
}
(二)懒汉式
为什么叫“懒汉式”:直到用了才提供对象(不着急~)
懒汉式实现步骤:
- 构造器私有化,防止外部new对象
- 定义一个static静态属性对象
- 提供一个public static 对象 getInstance()方法,可以返回一个对象
- 只有当用户使用getInstance()方法时,才会返回该对象,后面再次调用时,会返回上次创建的对象,从而保证了单例,且相比饿汉式来说,更节约资源。
代码实现:
public class SingleTon02 {
public static void main(String[] args) {
Fish fish1 = Fish.getInstance();
Fish fish2 = Fish.getInstance();
System.out.println(fish1);
System.out.println(fish2);
System.out.println(fish1 == fish2); // true
}
}
// 希望在程序运行中,只能创建一个Fish对象
class Fish {
private String name;
private Fish(String name) {
this.name = name;
}
private static Fish fish;
public static Fish getInstance() {
// 如果还没有创建Cat对象,就提供一个
if (fish == null) {
fish = new Fish("黄花鱼");
}
return fish;
}
@Override
public String toString() {
return "Fish{" +
"name='" + name + '\'' +
'}';
}
}
(三)饿汉式VS懒汉式区别
- 二者最主要的区别在于创建对象的时机不同:饿汉式是在类加载时就创建了对象实例,而懒汉式则是在使用时才创建。
- 饿汉式不存在线程安全问题,懒汉式存在线程安全问题:即当多个线程同时读取getInstance()方法时,可能同时判断对象==null。从而导致多个线程同时执行对象的创建。
- 饿汉式存在资源浪费的可能。因为如果一个对象实例都没有使用,那么饿汉式创建的对象就浪费了,懒汉式是使用时才创建,就不存在对象先创建而没有使用的这个问题。
- 在JavaSE标准类中,java.lang.Runtime就是经典的单例模式。代码如下:
六、final关键字
(一)基本介绍
final意为最后的、最终的。final可以修饰类、属性、方法和局部变量。
使用场景:
- 当不希望类被继承时,可以用final修饰;
- 当不希望父类的某个方法被子类覆盖/重写(override)时,可以用final关键字修饰;
- 当不希望类的某个属性的值被修改,可以用final修饰;
- 当不希望某个局部变量被修改,可以使用final修饰。
final修饰的类不能被继承:
final修饰的方法不能被重写:
final修饰的属性不能被修改:
final修饰的局部变量不能被修改:
(二)使用注意事项和细节讨论
(1)final修饰的属性又叫常量,一般用XX_XX_XX来命名。字母全部大写,不同的单词之间用_间隔,例如:TAX_RATE;
(2)final修饰的属性在定义时,必须赋初始值,并且以后不能再修改,赋值可以在如下位置之一:[选择一个位置赋初值即可]:
- 定义时:如public final double TAX_RATE=0.08;
- 在构造器中
- 在代码块中
class AA {
// 1.定义时直接赋值
public final double TAX_RATE = 0.08;
// 2.构造器中赋值
public final double TAX_RATE2;
public AA() {
TAX_RATE2 = 0.01;
}
// 3.代码块中赋值
public final double TAX_RATE3;
{
TAX_RATE3 = 1.1;
}
}
(3)如果final修饰的属性是静态的,则初始化的位置只能是a.定义时;b.在静态代码块中;不能在构造器中赋值。
(4)final类不能继承,但是可以实例化对象。
(5)如果类是final类,但是含有final方法,则该方法虽然不能重写,但是可以被继承。
(6)一般来说,如果一个类已经是final类了,就没有必要再将方法修饰成final方法。
(7)final不能修饰构造方法
(8)final和static往往搭配使用,效率更高,不会导致类加载,底层编译器做了优化处理
例如,当一个变量被static修饰时,调用该静态变量之前,会加载静态代码块中的内容,因为类被加载了(见图十)。 但是当这个变量被static和final同时修饰的时候,静态代码块就不会被加载(见图十一)。
图十 图十一
(9)包装类:Integer、Double、Float、Boolean等都是final修饰的;String、System也是final类。
(三)练习题
1.题目1
用3种方式给PI赋值,并求出圆的面积:
public class FinalExercise01 {
public static void main(String[] args) {
Circle circle = new Circle(5.0);
System.out.println(circle.ares()); // 78.5
}
}
class Circle {
private double radius;
//1.定义时赋值
private final double PI = 3.14;
// 2.构造器中赋值
private final double PI2;
public Circle(double radius) {
this.radius = radius;
PI2 = 3.14;
}
public Circle() {
PI2 = 3.14;
}
// 3.代码块中赋值
private final double PI3;
{
PI3 = 3.14;
}
public double ares() {
return PI * radius * radius;
}
}
2.题目2
判断下面代码是否有错误
class Something {
public int addOne(final int x) {
// ++x; // 错误,原因是不能修改 final x的值
// 下面的代码是可以的,因为基于它的当前值计算了一个新的值
// 没有给x赋予一个新的值
return x + 1;
}
}