1、自增变量
1.1 题目
public static void main(String[] args){
int i=1;
i = i++;
int j = i++;
int k = i + ++i * i++;
System.out.println("i="+i);
System.out.println("j="+j);
System.out.println("k="+k);
}
输出结果:
i=4
j=1
k=11
1.2 分析
i = i++;
:首先,赋值号右边的 i++ 先运算,因为++在右,所以先将 i 的值 1 压入操作数栈中,之后 i 变量自增变为 2(局部变量表中自增),此时 i 为 2;然后执行赋值号,将操作数栈中的 1 赋值给 i,就将 2 给覆盖了,所以此时 i 为 1。int j = i++;
:同理,i = 1 先压操作数栈,后自增,然后将操作数栈中的 1 赋值给 j,所以此时i=2,j = 1。int k = i + ++i * i++;
:- 先将 i=2 压入操作数栈中
- ++i 会先将局部变量表中的 i 自增变为 3,然后将 3 压入操作数栈
- i++ 先将 3 压入操作数栈中,后在局部变量表中自增为 4
- 此时的操作数栈中的值为:
3 3 2
- 先算乘除后算加减,所以将操作数栈中的两个3弹出来进行乘法运算,得 9 再次压回栈中
- 此时的栈中为
9 2
- 运算加法 2+9 = 11,再执行赋值运算,将 11 赋值给 k
- 所以结果为 i = 4,j = 1,k = 11
1.3 总结
- 赋值号最后运算
- 赋值号右边的 从左到右依次压入操作数栈
- 实际先算哪个,看运算符优先级
- 自增、自减操作都是直接修改变量的值,不经过操作数栈
- 最后的赋值之前,临时结果也是存储在操作数栈中的
1.4 练习
public static void main(String[] args) {
int i = 1;
int k = i + ++i * i++ + i++ * ++i; //1 + 2*2 + 3*5
System.out.println(k);// 结果为 20
}
2、单例设计模式(Singleton)
单例设计模式:即某个类在整个系统中只能有一个实例对象,可被获取和使用的代码模式。
例如:代表JVM运行环境的Runtime类
2.1 要点
- 该类只能有一个实例
- 构造器私有化
- 该类必须自行创建这个实例
- 含有一个该类的静态变量来保存这个唯一的实例
- 该类必须自行向整个系统提供这个实例
- 直接暴漏
- 静态变量的get方法
2.2 常见形式
2.2.1 饿汉式(直接创建对象,不存在线程安全问题)
2.2.1.1 直接创建实例对象,不管你是否需要这个对象
package javase;
/**
* 饿汉式:
* 直接创建实例对象,不管你是否需要这个对象都会创建
* (1)构造器私有化
* (2)自行创建,并用静态变量保存
* (3)向外提供这个实例
* (4)强调是一个实例,用final修饰
*/
public class Singleton01 {
public static final Singleton01 INSTANCE = new Singleton01();
private Singleton01() {
}
}
2.2.1.2 枚举式(最简洁)
package javase;
/**
* 枚举类型:标识该类型对象是有限的几个
* 限定为一个,就成了单例
*/
public enum Singleton02 {
INSTANCE
}
2.2.1.3 静态代码块
package javase;
import java.util.Properties;
/**
* 如果有额外参数从文件中读取使用
*/
public class Singleton03 {
public static final Singleton03 INSTANCE;
private String info;
static {
try {
Properties ps = new Properties(); ps.load(Singleton03.class.getClassLoader().getResourceAsStream("single.properties"));
INSTANCE = new Singleton03(ps.getProperty("info"));
} catch (Exception e) {
throw new RuntimeException(e);
}
}
private Singleton03(String info) {this.info = info; }
public String getInfo() {return info;}
public void setInfo(String info) {this.info = info;}
@Override
public String toString() {return "Singleton03[info="+info+"]";}
}
2.2.2 懒汉式(延迟创建对象)
2.2.2.1 第一版(线程不安全,适用于单线程)
package javase;
/**
* (1)构造器私有化
* (2)用一个静态变量保存唯一实例
* (3)提供一个静态方法获取去该实例
*/
public class Singleton04 {
private static Singleton04 INSTANCE;
private Singleton04() {}
public static Singleton04 getInstance() {
if(INSTANCE == null) {
INSTANCE = new Singleton04();
}
return INSTANCE;
}
}
测试:
public static void main(String[] args) throws Exception{
Callable<Singleton04> callable = new Callable<Singleton04>() {
@Override
public Singleton04 call() throws Exception {
return Singleton04.getInstance();
}
};
ExecutorService es = Executors.newFixedThreadPool(2);
Future<Singleton04> f1 = es.submit(callable);
Future<Singleton04> f2 = es.submit(callable);
Singleton04 s1 = f1.get();
Singleton04 s2 = f2.get();
System.out.println(s1 == s2);
System.out.println(s1);
System.out.println(s2);
}
结果:(概率发生)
原因:
在第一个线程进入了getInstance()方法中,经过if判断INSTANCE为null,在new之前,第二个线程进来了,因为此时还没有new,第二个线程也通过了if的判空,此时,第一个线程和第二个线程都会创建一个对象,即此时是两个对象。
2.2.2.2 第二版(线程安全,适用于多线程)
package javase;
/**
* 锁
*/
public class Singleton05 {
private static volatile Singleton05 INSTANCE;
private Singleton05() {}
public static Singleton05 getInstance() {
if(INSTANCE == null) {
synchronized (Singleton05.class) {
if(INSTANCE == null) {
INSTANCE = new Singleton05();
}
}
}
return INSTANCE;
}
}
2.2.2.3 第三版 静态内部类形式(适用于多线程)
package javase;
/**
* 在内部类被加载和初始化时才创建实例对象
* 静态内部类不会自动随着外部类的加载和初始化而初始化,它是要去单独加载和初始化的
* 因为时在内部类加载和初始化时创建的,因此是线程安全的
*/
public class Singleton06 {
private Singleton06() {}
private static class Inner {
private static final Singleton06 INSTANCE = new Singleton06();
}
public static Singleton06 getInstance(){
return Inner.INSTANCE;
}
}
2.3 总结
- 如果是饿汉式,枚举形式最简单
- 如果是懒汉式,静态内部类形式最简单
3、类初始化、实例初始化和方法重写
3.1 题目
代码:
package javase;
public class Father {
private int i = test();
private static int j = method();
static {
System.out.print("(1)");
}
public Father() {
System.out.print("(2)");
}
{
System.out.print("(3)");
}
public int test() {
System.out.print("(4)");
return 1;
}
public static int method() {
System.out.print("(5)");
return 1;
}
}
package javase;
public class Son extends Father{
private int i = test();
private static int j = method();
static {
System.out.print("(6)");
}
public Son() {
System.out.print("(7)");
}
{
System.out.print("(8)");
}
public int test() {
System.out.print("(9)");
return 1;
}
public static int method() {
System.out.print("(10)");
return 1;
}
public static void main(String[] args) {
Son s1 = new Son();
System.out.println();
Son s2 = new Son();
}
}
3.2 答案
(5)(1)(10)(6)(9)(3)(2)(9)(8)(7)
(9)(3)(2)(9)(8)(7)
3.3 分析
3.3.1 类初始化过程
- 一个类要创建实例需要先加载并初始化该类
- main方法所在的类需要先加载和初始化
- 子类初始化的需要先初始化父类
- 一个类初始化就是执行<clinit>()方法
- <clinit>()方法由静态类变量显示赋值代码和静态代码块组成
- 静态类变量显示赋值代码和静态代码块从上到下顺序执行
- <clinit>()方法只执行一次
所以,刚进入main方法时,首先是运行(5)(1)(10)(6)
3.3.2 实例初始化过程
- 实例初始化就是执行<init>()方法
- <init>()方法可以由很多个,有几个构造器就有几个<init>()方法
- <init>()方法由非静态实例变量显示复制代码和非静态代码块、对应构造器代码组成
- 非静态实例变量显示赋值代码和非静态代码块从上到下执行,对应构造器的代码最后执行
- 每次创建实例对象,调用对应构造器,执行的就是对应的<init>()方法
- <init>()方法的首行是supper()或supper(实参列表),即对应父类的<init>()方法
3.3.3 方法的重写Override
- 哪些方法不可以被重写
- final方法
- 静态方法
- private等子类中不可见的方法
- 对象的多态性
- 子类如果重写的父类的方法,通过子类对象调用的一定是子类重写过的方法()
- 非静态方法默认的调用对象是this
- this对象在构造器或者说<init>()方法中就是正在创建的对象
- 重写的要求
- 方法名、形参列表、返回值类型、抛出的异常列表、修饰符
3.4 总结
先加载父类的静态实例变量显示赋值代码和静态代码块,二者谁在前谁先执行;
再加载该类的静态实例变量显示赋值代码和静态代码块,二者谁在前谁先执行;
再执行父类的非静态实例变量显示赋值代码和非静态代码块,二者从上到下执行,如果初始化过程中该类重写了初始化时的调用的方法,则会执行子类重写的方法,对应构造器的代码最后执行;
再执行该类的非静态实例变量显示赋值代码和非静态代码块,二者从上到下执行,对应构造器的代码最后执行。
4、方法的参数传递机制
4.1 题目
package javase;
import javax.swing.text.ChangedCharSetException;
public class Exam4 {
public static void main(String[] args) {
int i = 1;
String str = "hello";
Integer num = 2;
int[] arr = {1,2,3,4,5};
MyData my = new MyData();
change(i,str,num,arr,my);
System.out.println("i="+i);
System.out.println("str="+str);
System.out.println("num="+num);
System.out.println("arr="+arr);
System.out.println("my.a="+my.a);
}
public static void change(int j,String s,Integer n,int[] a,MyData m) {
j += 1;
s += " world";
n += 1;
a[0] += 1;
m.a += 1;
}
}
class MyData{
int a = 10;
}
4.2 结果
i=1
str=hello
num=2
arr=2
my.a=11
4.3 分析
方法的参数传递机制
- 形参时基本数据类型
- 传递数据值
- 实参是引用数据类型
- 传递地址值
- 特殊的类型:String、包装类等对象不可变性
String类型在拼接的过程中是在常量池中开辟一个新的空间存放拼接后的字符串,并将这个新字符串的地址赋值给这个String类型的变量。
Integer在-128到127之间的时候操作原有对象,在范围之外之间创建新的对象,与String类似,不过是它是在堆中开辟空间。
因此,String和Integer类型不变。
int是基本类型,对象、数组是地址,所以会有这个结果。
5、迭代与递归
5.1 题目
有n步台阶,一次只能上1步或2步,共有多少种走法?
5.2 答案
5.2.1 递归(方法调用自身)
public int dg(int n) {
if(n<1) {
throw new IllegalArgumentException(n+"不能小于1");
}
if(n==1||n==2) {
return n;
}
return dg(n-1)+dg(n-2);
}
优点
- 大问题转化为小问题,可以减少代码量,同时代码精简,可读性好;
缺点
- 递归调用浪费空间,递归过深容易造成堆栈的溢出
5.2.2 迭代(利用原值推出新值)
public int dd(int n) {
if(n<1) {
throw new IllegalArgumentException(n+"不能小于1");
}
if(n==1||n==2) {
return n;
}
int temp1=1,temp2=2,sum=0;
for (int i = 3; i <= n; i++) {
sum = temp1+temp2;
temp1 = temp2;
temp2 = sum;
}
return sum;
}
优点
- 代码运行效率好,因为时间只因循环次数增加而增加,而没有额外的空间开销
缺点
- 代码不如递归简洁,可读性较差
6、成员变量与局部变量
6.1 题目
package javase;
public class Exam5 {
static int s;
int i;
int j;
{
int i=1;
i++;
j++;
s++;
}
public void test(int j) {
j++;
i++;
s++;
}
public static void main(String[] args) {
Exam5 obj1 = new Exam5();
Exam5 obj2 = new Exam5();
obj1.test(10);
obj1.test(20);
obj2.test(30);
System.out.println(obj1.i+" "+obj1.j+" "+obj1.s);
System.out.println(obj2.i+" "+obj2.j+" "+obj2.s);
}
}
6.2 结果
2 1 5
1 1 5
6.3 分析
局部变量与成员变量的区别
- 声明的位置
- 局部变量:方法体{}中,形参,代码块{}中
- 成员变量:类中方法外
- 类变量:有static修饰
- 实例变量:没有static修饰
- 修饰符
- 局部变量:final
- 成员变量:public、protected、private、final、static、volatile、transient
- 值存储的位置
- 局部变量:栈
- 实例变量:堆
- 类变量:方法区(java1.7之后方法区放进堆里,常量池也进堆里面)
- 作用域
- 局部变量:从声明处开始,到所属的}结束
- 实例变量:当前类“this.”(有时this.可缺省),在其他类中“对象名.”访问
- 类变量:在当前类中”类名.“(有时类名.可省略)”,在其它类中“类名.”或“对象.”访问
- 生命周期
- 局部变量:每个线程每次调用都是新的生命周期
- 实例变量:随着对象的创建而初始化,随着对象的被回收而消亡,每个对象的实例变量都是独立的
- 类变量:随着类的初始化而初始化,随着类的卸载而消亡,该类的所有对象的类变量是共享的
i 和 j 的结果通过作用域就可以计算出结果来,重点是 s 变量。
s变量为类变量,所有类共用。