一、自增变量
public class Main {
public static void main(String[] args) {
//i++是先把i压入栈,后对局部变量进行自增,++i是先对局部变量进行自增,后压入栈
int i = 1;//局部变量为1
i = i++;//先把1压入栈,局部变量i的值目前为1,自增以后,局部变量i的值变为2,赋值以后又变为1,所以这一步结果为1
int j = i++;//这一步和上一步雷同,i先压栈,后自增i变为2,再将栈中的1赋值给j变量
int k = i + ++i * i++;//将上一步i的结果2压栈,++i先把局部变量i的值变为3再压入栈中,i++是先将3压入栈中,再对局部变量i进行自增操作变为4,栈中得到结果为11
System.out.println(i);//4
System.out.println(j);//1
System.out.println(k);//11
}
}
小结:赋值最后计算,=右边的从左至右依次压入操作数栈,自增自减都是直接修改局部变量的值,不经过操作数栈,最后的赋值之前,临时结果也是存储在操作数栈中(i++就是先压栈后自增,++i就是先自增后压栈)
二、单例设计模式(编写一个Singleton示例)
要点:
- 某个类只能有一个实例:构造器私有化
- 必须自行创建这个实例:含有一个该类的静态变量来保存这个唯一的实例
- 必须自行向整个系统提供这个实例:(1)直接暴露 (2)用静态变量的get方法获取
几种常见形式:
1、饿汉式:直接创建对象,不存在线程安全问题
1)直接实例化饿汉式(简洁直观):在类初始化时直接创建实例对象,不管你是否需要这个实例对象,对象都会创建
public class Singleton1 {
public static final Singleton1 INSTANCE = new Singleton1();
private Singleton1() {
}
}
2)枚举
JDK1.5引入了新的类型——枚举。在 Java 中它虽然算个“小”功能,却给我的开发带来了“大”方便。
在JDK1.5 之前,我们定义常量都是: public static fianl....(这种方式在现在项目中也很常见) 。现在好了,有了枚举,可以把相关的常量分组到一个枚举类型里,而且枚举提供了比常量更多的方法。而且枚举类型可以帮助我们检测许多的编译失误。
在类加载的时候就初始化静态变量,默认调用私有构造方法去创建对象。
/*
* 枚举类型,表示该类型的对象是有限的几个
* 我们可以限定为一个,就成了单例
*/
public enum Singleton2 {
INSTANCE; //相当于public static final Singleton2 INSTANCE = new Singleton2();创建了一个静态常量,并在类初始化的时候去调用无参构造
private Singleton2(){} //默认调用这个私有的无参构造函数
}
3)静态代码块饿汉式(适合复杂实例化)
思考:如果把throw new RuntimeException(e) 这一行注释掉,则静态变量会显示未被初始化,但是加上这句话则可以,这是为什么呢?
个人理解:当时我就在想,如果常量一开始不赋初值的话,那么可以在静态代码块里赋值,而且如果把赋值语句写在try语句块外面,则不会报错,因为写在try语句块里面,编译器会认为走不到里面,所以会报错,但是如果你把可能出现的异常捕获并抛出,则编译器会认为try语句块不会出现异常,所以加上这句话即可实例化
public class Singleton3 {
public static final Singleton3 INSTANCE;
private String info;
static {
try {
Properties pro = new Properties();
pro.load(Singleton3.class.getClassLoader().getResourceAsStream("single.properties"));
INSTANCE = new Singleton3(pro.getProperty("info"));
} catch (IOException e) {
// e.printStackTrace();
throw new RuntimeException(e);
}
}
private Singleton3(String info){
this.info = info;
}
public static Singleton3 getINSTANCE() {
return INSTANCE;
}
public String getInfo() {
return info;
}
public void setInfo(String info) {
this.info = info;
}
@Override
public String toString() {
return "Singleton3{" +
"info='" + info + '\'' +
'}';
}
}
2、懒汉式:延迟创建对象
1)线程不安全(适用于单线程)
public class Singleton4 {
private static Singleton4 instance;
private Singleton4(){
}
public static Singleton4 getInstance(){
if(instance == null){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
instance = new Singleton4();
}
return instance;
}
}
public class test01 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
Callable<Singleton4> c = new Callable<Singleton4>() {
@Override
public Singleton4 call() throws Exception {
return Singleton4.getInstance();
}
};
/*
* JDK 5.0起提供了线程池相关API:ExecutorService 和 Executors
* ExecutorService: 真正的线程池接口。常见子类ThreadPoolExecutor
* void execute(Runnable command) : 执行任务/命令,没有返回值,一般用来执行Runnable
* <T> Future<T> submit(Callable<T> task):执行任务,有返回值,一般用来执行Callable
* void shutdown();关闭连接池
* Executors: 工具类、线程池的工厂类,用于创建并返回不同类型的线程池
* */
ExecutorService es = Executors.newFixedThreadPool(2);
Future<Singleton4> f1 = es.submit(c);
Future<Singleton4> f2 = es.submit(c);
Singleton4 s1 = f1.get();
Singleton4 s2 = f2.get();
System.out.println(s1==s2);
System.out.println(s1);
System.out.println(s2);
es.shutdown();
}
}
思路:结果为什么为false呢?因为第一个线程过来判断完是空以后,线程会被阻塞,阻塞完以后CPU就让出来了,另外一个线程也进来了,因为第一个线程还没new,所以它判断也是空,然后也被阻塞,线程1这时阻塞完了,就new出来,线程二阻塞完了也new出来,所以导致new了两次,所以结果为false。(当然这是个概率问题,也有可能会为true,这取决于你的CPU调度)
2)线程安全(适用于多线程)
利用了ClassLoader的线程安全机制。ClassLoader的loadClass方法在加载类的时候使用了synchronized关键字。
所以, 除非被重写,这个方法默认在整个装载过程中都是线程安全的。所以在类加载过程中对象的创建也是线程安全的。
public class Singleton5 {
private static Singleton5 instance;
private Singleton5(){
}
public static Singleton5 getInstance(){
if(instance == null) {
synchronized (Singleton5.class) {
if (instance == null) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
instance = new Singleton5();
}
}
}
return instance;
}
}
3)静态内部类形式(适用于多线程)
/*
* 在内部类被加载和初始化时,才创建INSTANCE实例对象
* 静态内部类不会自动随着外部类的加载和初始化而初始化,它是要单独去加载和初始化的。
* 因为是在内部类加载和初始化时,创建的,因此是线程安全的
* */
public class Singleton6 {
private Singleton6(){
}
private static class Inner{
private static final Singleton6 INSTANCE = new Singleton6();
}
public static Singleton6 getInstance(){
return Inner.INSTANCE;
}
}
小结:如果是饿汉式,枚举形式最简单
如果是懒汉式,静态内部类形式最简单
三、类初始化和实例初始化以及方法的重写
类初始化过程:
1、一个类要创建实例需要先加载并初始化该类
main方法所在的类需要先加载和初始化
2、一个子类要初始化需要先初始化其父类
3、一个类初始化就是执行<clinit>()方法
<clinit>()方法由静态类变量显示赋值代码和静态代码块组成
类变量显示赋值代码和静态代码块代码从上到下顺序执行
<clinit>()方法只执行一次
实例初始化过程:
- 实例初始化就是执行<init>()方法
- <init>()方法可能重载有多个,有几个构造器就有几个<init>方法
- <init>()方法由非静态实例变量显示赋值代码和非静态代码块、对应构造器代码组成
- 非静态实例变量显示赋值代码和非静态代码块代码从上到下顺序执行,而对应构造器的代码最后执行
- 每次创建实例对象,调用对应构造器,执行的就是对应的<init>方法
- <init>方法的首行是super()或super(实参列表),即对应父类的<init>方法
方法的重写Override:
1、哪些方法不可以被重写
- final 方法
- 静态方法
- private等子类中不可见方法
2、对象的多态性:
- 子类如果重写了父类的方法,通过子类对象调用的一定是子类重写过的代码
- 非静态方法默认的调用对象是this
- this对象在构造器或者说<init>方法中就是正在创建的对象
类的继承知识点
(1)java不支持多重继承,也就是说子类至多只能有一个父类
(2)子类继承了其父类中不是私有的成员变量和成员方法,作为自己的成员变量和方法
(3)子类中定义的成员变量和父类中定义的成员变量相同时,则父类中的成员变量不能被继承
(4)子类中定义的成员方法,并且这个成员方法的名字,返回类型,及参数个数和类型与父类的某个成员方法完全相同,则父类的成员方法不能被继承。
继承中构造方法的关系
子类中所有的构造方法默认都会访问父类中空参数的构造方法
因为子类会继承父类中的数据,可能还会使用父类的数据,所以子类初始化之前,一定要先完成父类数据初始化
注意:子类每一个构造方法的第一条语句默认都是super();
方法重写的要求:
重写发生在运行期,是子类对父类的允许访问的方法的实现过程进行重新编写。
- 返回值类型、方法名、参数列表必须相同,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类。
- 如果父类方法访问修饰符为
private/final/static
则子类就不能重写该方法,但是被 static 修饰的方法能够被再次声明。 - 构造方法无法被重写
综上:重写就是子类对父类方法的重新改造,外部样子不能改变,内部逻辑可以改变
重载就是同一个类中多个同名方法根据不同的传参来执行不同的逻辑处理。
重写重载区别:
区别点 | 重载方法 | 重写方法 |
---|---|---|
发生范围 | 同一个类 | 子类 |
参数列表 | 必须修改 | 一定不能修改 |
返回类型 | 可修改 | 子类方法返回值类型应比父类方法返回值类型更小或相等 |
异常 | 可修改 | 子类方法声明抛出的异常类应比父类方法声明抛出的异常类更小或相等; |
访问修饰符 | 可修改 | 一定不能做更严格的限制(可以降低限制) |
发生阶段 | 编译期 | 运行期 |
示例代码:
/*
* 父类的初始化<clinit>;
* (1) j = method();
* (2) 父类的静态代码块
*
* * 父类的实例化方法:
* (1) super();(最前)
* (2) i = test();
* (3) 父类的非静态代码块
* (4) 父类的无参构造(最后)
*
* 非静态方法前面其实有一个默认的对象this
* this在构造器 (或<init>) 它表示的是正在创建的对象,因为这里是在创建Son对象,所以
* test()执行的是子类重写的代码
*/
public class Father {
private int i =this.test();
private static int j = method();
static {
System.out.print("(1)");
}
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;
}
}
/*
* 子类的初始化<clinit>;
* (1) j = method();
* (2) 子类的静态代码块
* 先初始化父类 5 1
* 初始化子类 10 6
*
* 子类的实例化方法:
* (1) super();(最前)9 3 2
* (2) i = test(); 9
* (3) 子类的非静态代码块 8
* (4) 子类的无参构造(最后) 7
* 因为创建了两个Son对象,因此实例化方法<init> 执行两次
* 9 3 2 9 8 7
*/
public class Son extends Father {
private int i = test();
private static int j = method();
static {
System.out.print("(6)");
}
Son(){
super();//写或不写都在,在子类构造器中一定会调用父类的构造器
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();
}
}
执行结果:
(5)(1)(10)(6)(9)(3)(2)(9)(8)(7)
(9)(3)(2)(9)(8)(7)
Process finished with exit code 0
四、方法的参数传递机制
1、形参是基本数据类型
实参传递数据值给形参
2、形参是引用数据类型
实参传递地址值给形参
特殊的类型:String、包装类等对象不可变性
示例:
public class Exam4 {
public static void main(String[] args) {
int i = 1;
String str = "hello";
Integer num = 200;
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 = "+ Arrays.toString(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;
}
结果:
i=1
str = hello
num = 200
arr = [2, 2, 3, 4, 5]
my.a =11
五、递归与循环迭代
编程题:有n步台阶,一次只能上1步或2步,共有多少种走法?
一:递归:
public class TestStep {
public int f(int n){
if(n<1){
throw new IllegalArgumentException(n + "不能小于1");
}
if(n==1 || n==2){
return n;
}
return f(n-2) + f(n-1);
}
}
二: 迭代:
public class TestStep2 {
public int loop(int n){
if(n<1){
throw new IllegalArgumentException(n + "不能小于1");
}
if(n==1 || n==2){
return n;
}
int one = 1;//初始化为走到第一级台阶的走法
int two = 2; //初始化为走到第二级台阶的走法
int sum = 0;
for(int i = 3;i<=n;i++){
//最后跨1步 + 最后跨2步的走法
sum = one + two;
one = two;
two = sum;
}
return sum;
}
}
小结:
方法调用自身称为递归,利用变量的原值推出新值称为迭代。
递归:
优点:大问题转化为小问题,可以减少代码量,同时代码精简,可读性好;
缺点:递归调用浪费了空间,而且递归太深容易造成堆栈的溢出。
迭代:
优点:代码运行效率好,因为时间只因循环次数增加而增加,而且没有额外的空间开销;
缺点:代码不如递归简洁,可读性好
六、成员变量与局部变量
局部变量与成员变量的区别:
1、声明的位置
局部变量:方法体{}中,形参,代码块{}中
成员变量:类中方法外
- 类变量:有static修饰
- 实例变量:没有static修饰
2、修饰符
局部变量:final
成员变量:public、protected、private、final、static、volatile、transient
3、值存储的位置
局部变量:栈
实例变量:堆
类变量:方法区
4、作用域
局部变量:从声明处开始,到所属的}结束
实例变量:在当前类中“this.”(有时this.可以缺省),在其他类中"对象名."访问
类变量:在当前类中"类名."(有时类名.可以省略),在其他类中"类名."或"对象名."访问
5、生命周期:
局部变量:每一个线程,每一次调用执行都是新的生命周期
实例变量:随着对象的创建而初始化,随着对象的被回收而消亡,每一个对象的实例变量是独立的
类变量:随着类的初始化而初始化,随着类的加载而消亡,该类的所有对象的类变量是共享的
当局部变量与xx变量重名时,如何区分:
1、局部变量与实例变量重名:
在实例变量前面加"this."
2、局部变量与类变量重名:
在类变量前面加"类名."
示例代码:
public class Exam5 {
static int s; //成员变量,类变量
int i;//成员变量,实例变量
int j;//成员变量,实例变量
{
int i = 1;//非静态代码块中的局部变量 i
i++;
j++;
s++;
}
public void test(int j){//形参,局部变量,j
j++;
i++;
s++;
}
public static void main(String[] args) {//形参,局部变量,args
Exam5 obj1 = new Exam5();//局部变量,obj1
Exam5 obj2 = new Exam5();//局部变量,obj2
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);
}
}
运行结果:
2,1,5
1,1,5