1、private
使用private实现封装
- 如果使用private对属性进行封装,要访问私有属性,必须提供setter和getter方法。
setter方法:主要用于属性内容的设置与修改
getter方法:主要用于属性内容的获取 - private实现封装最大的特征在于:只允许本类访问,而不允许外部类访问
类的设计原则:
1)编写类时,类中的所有属性必须使用private封装
2)属性若要被外部访问,必须提供setter,getter方法
2、this
this关键字主要有三个方面的用途:
1)调用本类属性
2)调用本类方法
3)表示当前对象
2.1 this调用本类属性
this.name = name;
this.age = age;
只要在类中方法访问类中属性,一定要加this关键字。
2.2 this调用本类方法
- 调用普通方法:this . 方法名称(参数)
- 调用构造方法:this(参数)
使用this调用构造方法时要注意:
1)this调用构造方法的语句必须在构造方法首行
2)使用this调用构造方法时,要留有出口,不能构成环
2.3 this表示当前对象
class Person{
public void print(){
System.out.println("[print]方法:"+this);
}
}
public class Test {
public static void main(String[] args) {
Person p1 = new Person();
System.out.println("[Main]方法:"+p1);
p1.print();
System.out.println("==================");
Person p2 = new Person();
System.out.println("[Main]方法:"+p2);
p2.print();
}
}
当前调用的属性或方法是通过哪个对象调用的,this就指代它。
3、static关键字(***)
3.1 static类属性
非static属性称为成员变量或对象属性,每个对象都有此属性且值都不一样。
static属性又称类属性,静态属性,保存在全局数据区的内存中,所有对象都可以进行该数据区的访问。描述共享属性,只需在属性前面加上static关键字即可。
比如,在上面country这个属性前面加上static:
static String country = "中国";
内存分析图变为:
结论:
- 访问static属性(类属性)应使用 类名 . 属性名,比如Person.country = “中华民国”,此时所有对象都共享此属性值。
- 所有非static属性(实例变量)必须在对象实例化后使用,而static属性不受实例化控制
3.2 static类方法
使用static定义的方法,直接通过类名称访问,与对象实例化无关。
注意:
- 所有的static方法
不允许
调用非static定义的属性或方法 - 所有的非static方法
允许
访问static方法或属性 - 所有的static方法可以访问static属性
- 静态方法常见于工具方法。
eg:java.util.Arrays.sort();
System.arraycopy();
static修饰的东西被我们称为类成员,它会随着类的加载而加载。
static也可以修饰内部类:
因为内部类算是类的成员,如果我们没有使用static来修饰,那么我们在创建内部类的时候就需要先有一个外部类的对象,如果我们一直在使用内部类,那么内存中就会一直存在外部类的引用,而我们有时候只需要使用内部类,不需要外部类,就会浪费内存,甚至会造成内存溢出。使用static修饰内部类之后,内部类在创建对象时就不需要有外部类对象的引用了。
(static不可以修饰外部类,没有意义!)
4、super关键字
4.1 表示父类属性
super . 属性名称:明确表示直接从父类中查找属性
4.2 表示父类方法
1)表示父类构造方法 super(方法参数)
- 调用父类的无参构造时,子类构造方法首行super()可以省略
- 当父类不存在无参构造时,子类构造方法中必须使用super(方法参数),明确表示当前调用的是哪个父类构造,super不能省略,且必须处于子类构造方法首行,此时子类不存在this构造器调用。
2)表示父类被覆写的方法 super.方法()
使用super调用父类的同名方法
class Person {
public void print() {
System.out.println("1、I am father");
}
}
class Student extends Person {
public void print() {
super.print();
System.out.println("2、I am child");
}
}
public class Test {
public static void main(String[] args) {
new Student().print();
}
}
使用super调用父类的属性
class Person {
public String info = "爸爸";
}
class Student extends Person {
public String info = "儿子";
public void print() {
System.out.println(super.info);
System.out.println(this.info);
}
}
public class Test {
public static void main(String[] args) {
new Student().print();
}
}
this和super的区别:
- 概念
this访问本类中的属性和方法,super由子类访问父类中的属性和方法 - 查找范围
this先查找本类,如果本类没有就调用父类;super不查找本类直接调用父类的定义。 - 特殊
this表示当前对象
5、final关键字
在Java中final被称为终结器。
使用final修饰类
1)使用final定义的类不能有子类(String类就是使用final定义的)
2)final一旦修饰一个类之后,该类的所有方法默认都会加上final修饰(不包含成员变量)使用final修饰方法 --- 封装
1)使用final定义的方法不能被子类覆写使用final修饰属性 --- 常量
1)使用final定义的变量就成了常量,常量必须在声明时赋值,并且不能被修改
2)Java中常量一般使用final和static共同修饰
3)final成员变量必须在声明的时候初始化或者在构造器中初始化,否则会报编译错误
4)定义常量(public static final),常量全用大写字母,多个单词以 _ 分隔
public static final MAX_AGE = 120;
5)final修饰数据类型,无论是基本类型,还是引用类型,值不能改变,对于引用数据类型而言,不可改变的是指向不能变(保存的堆内存地址不能改变)
6、throw和throws关键字
6.1 throw
throw直接编写在语句中,表示人为进行异常的抛出。如果现在异常类对象实例化不希望JVM产生而由用户产生,就可以使用throw来完成。
throw用在方法中:
public static void main(String[] args){
try{
throw new Exception("抛个异常玩玩!");
}catch(Exception e){
e.printStackTrace();
}
}
一般来说,throw 和 break,continue 都要和 if 结合使用。
6.2 throws
在进行方法定义的时候,如果要告诉调用者本方法可能产生哪些异常,就可以使用throws方法进行声明。即如果该方法出现问题后不希望进行处理,就使用throws抛出。
throws用在方法上:
public class Test {
public static void main(String[] args) {
try{
System.out.println(calculate(10,0));
}catch (Exception e){
e.printStackTrace();
}
}
public static int calculate(int x,int y)throws Exception{
return x/y;
}
}
主方法本身也属于一个方法,所以主方法上也可以使用throws进行异常抛出,这个时候如果产生了异常就会交给JVM处理。
6.3 throw和throws的区别:
- throw用于方法内部,主要表示手工异常抛出
- throws主要在方法声明上使用,明确告诉用户本方法可能产生的异常,同时该方法可能不处理这个异常
7、volatile关键字
当一个变量使用volatile
关键字定义之后,它有两个特性:
1)保证此变量对所有线程可见
保证变量可见有三种方法:
- synchronized(Lock)
- final
- volatile
"可见性"是指:当一条线程修改了这个变量的值,新值对于其他线程来说是可以立即得知的。而普通变量做不到这一点,普通变量的值在线程间传递均需要通过主内存来完成。例如:线程A修改一个普通变量的值,然后向主内存进行回写,另外一条线程B在线程A回写完成之后再从主内存进行读取操作,新值才会对线程B可见。
关于volatile变量的可见性,经常会被开发人员误解。volatile变量在各个线程中是一致的,但是volatile变量的运算在并发下一样是不安全的。原因在于Java里面的运算并非原子操作。
由于volatile关键字只保证可见性,在不符合以下两条规则的运算场景中,我们仍然需要通过加锁(synchronized或 者lock)来保证原子性。
1)运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值
2)变量不需要与其他的状态变量共同参与不变约束
如下代码这类场景就特别适合使用volatile来控制并发,当shutdown()方法被调用时,能保证所有线程中执行的 doWork()方法都立即停下来。
//(当线程1调用shutdown方法,线程2可以立即停止,不会有延迟)
volatile boolean shutdownRequested;
// 线程1
public void shutdown() {
shutdownRequested = true;
}
public void work() {
// 线程2
while(!shutdownRequested) {}
}
2)禁止指令重排序(内存屏障)
普通的变量仅仅会保证在该方法的执行过程中所有依赖赋值结果的地方都能获取到正确的结果,而不能保证变量赋值操作的顺序和程序代码中执行的顺序一致。
volatile关键字禁止指令重排序有两层意思:
- 当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;
- 在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。
举个栗子:
int i = 1;
int y = 2;
volatile boolean flag = true;
i = i + 3;
y = y + 4;
上面这段代码保证了两点:
1)第三行既不会提前也不会滞后
2)当CPU运行到第三行代码,可以保证前面的代码一定全部执行完毕且结果对后序可见;
之后的代码还未开始执行
8、synchronized关键字
关于synchronized在这篇文章中有详细介绍:https://blog.youkuaiyun.com/Nan_Feng726/article/details/97635498
这里说几道常考的题:
class Task implements Runnable {
public synchronized void testA() {
System.out.println("A!");
while (true) {
}
}
public synchronized void testB() {
System.out.println("B!");
}
@Override
public void run() {
if (Thread.currentThread().getName().equals("A")) {
testA();
} else {
testB();
}
}
}
上面这段代码的输出结果为A!。
原因是synchronized同步方法,对象锁锁的是当前Task类的对象:this,A线程首先执行,当它进入同步方法后,锁住当前对象并且不释放锁,而B线程要进入同步方法的前提是拿到当前对象锁的monitor,但是A一直没有释放,所以B会一直卡着。输出A!
如果将代码改成这样呢:
class Task implements Runnable {
public synchronized void testA() {
System.out.println("A!");
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public synchronized void testB() {
System.out.println("B!");
}
@Override
public void run() {
if (Thread.currentThread().getName().equals("A")) {
testA();
} else {
testB();
}
}
}
输出结果为:输出A!B!
很明显这道题是在上题的基础上进行了改造,TimeUnit.SECONDS.sleep(2);
,2秒后A线程执行完毕,释放锁,B就可以进同步方法了,所以输出A!B!
接着对代码进行改造:
class Task implements Runnable {
public synchronized void testA() {
System.out.println("A!");
testB();
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public synchronized void testB() {
System.out.println("B!");
}
@Override
public void run() {
if (Thread.currentThread().getName().equals("A")) {
testA();
} else {
testB();
}
}
}
输出结果为:输出A!B!B!
这道题体现了synchronized的可重入性。打印完A!要执行testB()了,执行前判断能不能拿到当前对象的monitor,虽然monitor不为0,但是持有锁的线程就是当前线程,所以可以进入,monitor加1。打印完B!,testA()执行完毕,睡2秒后执行testB(),再次打印B!,所以最终结果为A!B!B!
下面这段代码输出的时间间隔是多少?
class Task implements Runnable {
public synchronized void testA() {
System.out.println("A!");
testB();
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public synchronized void testB() {
System.out.println("B!");
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Override
public void run() {
if (Thread.currentThread().getName().equals("A")) {
testA();
} else {
testB();
}
}
}
public class SychronizedTest {
public static void main(String[] args) {
Task task = new Task();
Thread thread1 = new Thread(task, "A");
Thread thread2 = new Thread(task, "B");
thread1.start();
thread2.start();
}
}
输出结果为A!B!B!,并且两次打印B!的间隔为4秒。
因为加锁了两次,B释放后A的monitor还要减一,A线程中睡了2秒,B线程中也睡了2秒,所以两次打印的间隔为4秒。