Java基础知识回顾之二 ----- 修饰符和String
前言
在上一篇中,回顾了Java的基本数据类型 ,这篇就来回顾下Java中的一些修饰符以及String。
修饰符介绍
Java修饰符主要分为两类:
- 访问修饰符
- 非访问修饰符
其中访问修饰符主要包括 private、default、protected、public。
非访问修饰符主要包括 static、final、abstract、synchronized。
访问修饰符
访问修饰符可以使用下图这张表来说明访问权限:
修饰符 | 当前类 | 同一包内 | 子类 | 其它包 |
---|---|---|---|---|
public | Y | Y | Y | Y |
protected | Y | Y | Y | N |
default | Y | Y | N | N |
private | Y | N | N | N |
简单点查看访问级别的话,级别是由低到高。
private<default<protected<public
private
被private修饰的变量、方法仅限在本类中使用。
所以private是最严的的访问级别,主要用于隐藏类的一些细节实现和保护类的数据。
例如pojo类就是使用private修饰变量,对外提供setter和getter的方法。
还有如果使用private用来修饰构造方法的话,该类是不能实例化的。这种在单例模式中可以经常看到!
虽然private主要用于修饰变量和方法,不过也可以修饰内部类,只不过是内部类。
例如:
public class Test{
//修饰一个私有变量
private int count=1;
//修饰一个私有方法
private int add(int i,int j){
return i+j;
}
private class Test1{
}
}
注意:private不能修饰外部类。
因为Test类中的变量和方法是私有的,所以其他类无法调用!
例:
public class Test2 {
public static void main(String[] args) {
Test t=new Test();
//下面的变量和方法是无法获取的
//t.count=2;
//t.add(1,2);
}
}
说明:其实private修饰的方法和变量是可以使用反射调用,不过这里就不说明了。
default
default:就是不使用任何修饰符。类、接口、变量、方法都可以使用。不过仅限在同一包下。
例如:
class Test{
int count=1;
int add(int i,int j){
return i+j;
}
interface Test1{
}
}
protected
被protected修饰的变量、方法仅仅对同一包内的类和所有子类可见。
例如:
public class Test{
protected int count=1;
protected int add(int i,int j){
return i+j;
}
protected class Test1{
}
}
在同包下可以直接调用,如果不在同包,则需要继承才可以使用。
public class Test2 extends Test{
public static void main(String[] args) {
Test t=new Test();
t.count=2;
t.add(1,2);
}
}
注意:protected不能修饰外部类。
public
public:修饰的类、接口、变量、方法对所有类都可以使用。
例如:
public class Test{
public int count=1;
public int add(int i,int j){
return i+j;
}
}
非访问修饰符
为了实现一些其他的功能,Java 也提供了许多非访问修饰符。
static
static: 用来修饰类变量和类方法。
静态变量:
static在修饰类变量的时候,无论该类被实例化了多少次,它的静态变量只有一份拷贝。静态变量也被称为类变量。局部变量是不能被声明为static变量的。
静态方法:
static在修饰类方法的时候,静态方法是不能使用类的非静态变量。静态方法可以直接通过类名调用,因此静态方法中是不能用this和super关键字的。
示例:
public class Test{
public String name="xuwujing";
public static String name2="xuwujing";
public static String getName() {
//这个一句 会报错 因为静态方法是不能使用类的非静态变量
//String reult=name;
//这一句就可以
String reult=name2;
return reult;
}
//main方法是静态方法,可以直接调用本类中的静态方法和静态变量
public static void main(String[] args) {
System.out.println(name2);
System.out.println(getName());
}
//该方法是不静态方法,所以调用本类中的静态方法和静态变量时,
//需要使用classname.variablename和 classname.methodname的方式访问
private void print(){
System.out.println(Test.name2);
System.out.println(Test.getName());
}
}
在这里顺便提一下,static 静态块。
在JVM类加载机制中,如果类存在直接的父类并且这个类还没有被初始化,那么就先初始化父类;如果类中存在初始化语句,就依次执行这些初始化语句。
可能上述的两句话不太好理解,那么这里我们来运行下代码查看其结果,通过结果可能就能更好的理解上述语句的话了。
示例:
class HelloA {
public HelloA() {
System.out.println("HelloA");
}
{ System.out.println("I'm A class"); }
static { System.out.println("static A"); }
}
public class HelloB extends HelloA{
public HelloB() {
System.out.println("HelloB");
}
{ System.out.println("I'm B class"); }
static { System.out.println("static B"); }
public static void main(String[] args) {
new HelloB();
}
结果:
static A
static B
I'm A class
HelloA
I'm B class
HelloB
那么根据这个类返回的结果是不是感觉更好理解了呢?
创建对象时构造器的调用顺序是:
先初始化静态成员,然后调用父类构造器,再初始化非静态成员,最后调用自身构造器。
那么static修饰符这块的运用可以总结如下:
- 静态变量在内存中只有一个拷贝,在类的所有实例中共享。
- 在静态方法中不能直接访问实例方法和实例变量,反之可以。
- 在静态方法中不能使用this和super关键字。
- 静态方法不能被abstract修饰。
- 静态方法和静态变量都可以通过类名直接访问。
- 当类被加载时,静态代码块只被加载一次。有多个静态变量或块时,按声明顺序加载。
final
final :用来修饰类、方法和变量。
final 修饰的类不能够被继承,修饰的方法不能被继承类重新定义,修饰的变量为常量,是不可修改的。
如果上述语句不好理解的话,我们可以通过编写相关代码进行实验。
定义一个final修饰的变量、方法以及类。然后进行相关的测试
示例:
public class Test{
//定义一个final修饰的变量
public static final String name="xuwujing";
public static void main(String[] args) {
//这句会报错 因为该变量已经被final修饰了
name="张三";
}
//类加上final之后,该类是无法被继承的
final class Test2{
}
//这句会报错,因为Test2是被final修饰的类
class Test3 extends Test2{
}
class Test4{
//定义一个被final修饰的方法
final Date getTime(){
return new Date();
}
}
class Test5 extends Test4{
//这句会报错,因为final方法是不能被子类修改的。
Date getTime(){
return new Date();
}
}
}
从上述 代码结果,我们可以得出一下结论:
final修饰类:表示该类不能被继承;
final修饰方法:表示方法不能被重写;
final修饰变量:表示变量只能一次赋值以后值不能被修改(常量);
abstract
abstract :用来创建抽象类和抽象方法。
Java是面向对象的语言,而抽象类是Java语言中对抽象概念进行定义的一种机制,也正是因为这个,所以赋予了Java强大的面向对象的能力。
修饰类
会使这个类成为一个抽象类,这个类将不能生成对象实例,但可以做为对象变量声明的类型(见后面实例),也就是编译时类型。抽象类就相当于一类的半成品,需要子类继承并覆盖其中的抽象方法。
修饰方法
会使这个方法变成抽象方法,也就是只有声明而没有实现,需要子类继承实现。
这里依旧使用一个简单例子来进行理解。
public class AbstractTest{
public static void main(String[] args) {
//这句会报错,因为抽象类不能实例化
// Animal a=new Animal();
//抽象类可以实例化重写该类抽象方法的子类
Animal a = new Dog();
a.show();
}
}
abstract class Animal{
abstract void show();
public void print(){
System.out.println("Animal");
}
}
//继承抽象类需要实现抽象类的方法
class Dog extends Animal{
@Override
void show() {
System.out.println("This is Dog!");
}
}
总结:
1、抽象类和抽象方法都需要被abstract修饰。抽象方法一定要定义在抽象类中。
2、抽象类不可以创建实例,原因:调用抽象方法没有意义。
3、只有覆盖了抽象类中所有的抽象方法后,其子类才可以实例化。否则该子类还是一个抽象类。
注意事项:
1、抽象类不能用来实例化对象,声明抽象类的唯一目的是为了将来对该类进行扩充。 2、一个类不能同时被 abstract 和 final
修饰。如果一个类包含抽象方法,那么该类一定要声明为抽象类,否则将出现编译错误。
3、抽象方法是一种没有任何实现的方法,该方法的的具体实现由子类提供。 4、抽象方法不能被声明成 final 和 static。
5、任何继承抽象类的子类必须实现父类的所有抽象方法,除非该子类也是抽象类。
6、如果一个类包含若干个抽象方法,那么该类必须声明为抽象类。抽象类可以不包含抽象方法。
synchronized
synchronized: 修饰的方法同一时间只能被一个线程访问。在多线程中运用很常见。
synchronized 的解释如下:
synchronized 方法控制对类成员变量的访问:每个类实例对应一把锁,每个 synchronized方法都必须获得调用该方法的类实例的锁方能执行,否则所属线程阻塞,方法一旦执行,就独占该锁,直到从该方法返回时才将锁释放,此后被阻塞的线程方能获得该锁,重新进入可执行状态。这种机制确保了同一时刻对于每一个类实例,其所有声明synchronized 的成员函数中至多只有一个处于可执行状态(因为至多只有一个能够获得该类实例对应的锁),从而有效避免了类成员变量的访问冲突(只要所有可能访问类成员变量的方法均被声明为 synchronized)。
简单的来说,就是使用synchronized 修饰的方法,在多线程进行同时访问的时候,只会让一个线程先进行访问,其它的线程等候,当这个线程访问完了之后,再让下一个进行访问,依次类推。
Java中还有两个不太常见的修饰符,transient 和native。
transient:被 transient 修饰的实例变量时,java 虚拟机(JVM)跳过该特定的变量。
native: 被native修饰的方法实际是由另一种语言进行实现的本地方法。例如Java中获取的Long类型的时间戳 :System.currentTimeMillis();
实际是由native 修饰的,
源码为:
public static native long currentTimeMillis();
String
String 类型可能就是我们最常用的的对象了。
首先说明,String并不是基本数据类型,而是一个对象,并且是不可变的对象。查看源码可以String类是被final修饰的,是不可被继承的!
String的在未被初始化的时候为null,表示它还没有被创建,自然也就没有分配空间;
而" "和 new String()不是null,它们是已经被创建,只是值为空而已!并且也分配了内存空间。
String有15种构造方法,有两种是过时的,其中包含char[],byte[],int[],String,StringBuffer,StringBuilder。
我们在创建String对象的的时候,一般是使用 String str="xxx",但有时也会用new String()来初始话字符串。
例如:
String hello="hello";
String newHello=new String("hello");
char []cHello ={'h','e','l','l','o'};
String str=new String(cHello);
注意:String 类是不可改变的,所以你一旦创建了 String 对象,那它的值就无法改变了。
String常用方法
大概讲述了String的用法之后,这里我们来列举一些String常用的方法。
1.length :返回此字符串的长度。
2.charAt:返回指定索引处的 char 值。
3.compareTo:把这个字符串和另一个对象比较。
4.concat:将指定字符串连接到此字符串的结尾。
5.split:根据给定正则表达式的匹配拆分此字符串。
6.equals:将此字符串与指定的对象比较。
7.endsWith:测试此字符串是否以指定的后缀结束。
8.startsWith:测试此字符串是否以指定的前缀结束。
9.getBytes: 使用平台的默认字符集将此 String 编码为 byte 序列,并将结果存储到一个新的 byte 数组中。
10.indexOf:返回指定字符在此字符串中第一次出现处的索引。
11.replace:返回一个新的字符串,它是通过用 newChar 替换此字符串中出现的所有 oldChar 得到的。 12:substring:返回一个新的字符串,它是此字符串的一个子字符串。
...
更多可以参考Api文档。
String对象比较
String作为我们最常用的对象,在面试中估计也会接触不少。一般来说,会考到String的常量池相关问题,主要是使用String进行比较的时候,==和equals这两种方法来判断是否相当。这里收集了一些String经常遇到的问题。
代码如下:
String s1 = "test";
String s2 = new String("test");
String s3 = "te";
String s4 = "st";
String s5 = "te" + "st";
String s6 = s3 + s4;
String s7 = new String(s1);
System.out.println(s1 == s2);
System.out.println(s1 == s5);
System.out.println(s1 == s6);
System.out.println(s7==s1);
System.out.println(s7.equals(s1));
结果:
false
true
false
false
true
如果有经验的话,大概可以一眼看出结果。但是如果经验不足的话,往往会吃这个亏。这里来解释下为什么会出现这种结果。
1.虽然看起来是一样的,但是新建一个String类的时候会重新分配引用地址,而 == 就是比较引用地址,所以为false。
2.在编译之前就可以确认s5=test, 并且引用地址一样,所以为true;
3.字符串常量池的原则 这时 s6 的值是在运行时得到的,它会重新构造字符串对象 所以为false。
4.和第一个一样的,就是换汤不换药,所以为false。
5.equals 只比较值相等,不关心它的引用地址。
看完上面的例子之后,再来看看下面的这个
代码示例:
String ab="ab";
String c="c";
String ab_c=ab+c;
String ab_c1="ab"+"c";
String abc="abc";
System.out.println(ab_c == abc + " : " + ab_c.equals(abc));
System.out.println((ab_c == abc) + " : " + ab_c.equals(abc));
System.out.println((ab_c1 == abc) + " : " + ab_c1.equals(abc));
运行结果:
false
false : true
true : true
到这里,可能就会诧异了,为什么和我想的不一样呢?
这里其实是有陷阱的,也就是运算符的优先级。
第一个结果就是优先级的问题导致的,它会先计算 abc + " : " + ab_c.equals(abc)
,然后再来进行比较,所以为false。同理,下面的也是如此,基本和上面的那个例子差不多,这里就不再概述了。
String、StringBuffer和StringBuilder
String、StringBuffer和StringBuilder的区别:
- String: String的特点是一旦赋值,便不能更改其指向的字符对象,如果更改,则会指向一个新的字符对象。
- StringBuffer:StringBuffer对象可以调用其方法动态的进行增加、插入、修改和删 除操作,且不用像数组那样事先指定大小,从而实现多次插入字 符,一次整体取出的效果,因而操作字符串非常灵活方便。并且生成数据之后可以toString转为String,线程安全。
- StringBuilder:它是在单线程环境下使用的,因为它的所有方面都没有被synchronized修饰,因此它的效率也比StringBuffer要高。
关于字符串拼接方式,在String类中,我们最常用的是 + ,其次是使用StringBuffer或StringBuilder 的append方法,至于String类中的concat几乎很少用到。
一般来说,如果在少量的字符串进行拼接的话,我们会使用+,如果拼接过多的话,单线程使用 StringBuilder ,多线程使用StringBuffer 进行拼接。因为使用String 的 + 在过多的字符串进行拼接的时候会极大的使用内存,因为它在凭借的时候还是使用 append()方法,然后再进行toString转换,如果是少量的时候,是感觉不到差异的,但是在大量拼接的时候就会明显感受得到。
代码示例:
String str="Hello World";
String str1="";
StringBuffer sbr=new StringBuffer(str);
StringBuilder sbd=new StringBuilder(str);
long start=System.currentTimeMillis();
for(int i=0;i<10000;i++){
str1+=str;
}
System.out.println("String累加用时:"+(System.currentTimeMillis()-start)+"ms");
long start2=System.currentTimeMillis();
for(int i=0;i<10000;i++){
sbr.append(str);
}
System.out.println("StringBuffer累加用时:"+(System.currentTimeMillis()-start2)+"ms");
long start3=System.currentTimeMillis();
for(int i=0;i<10000;i++){
sbd.append(str);
}
System.out.println("StringBuilder累加用时:"+(System.currentTimeMillis()-start3)+"ms");
结果:
String累加用时:701ms
StringBuffer累加用时:2ms
StringBuilder累加用时:0ms
这里从输出结果中可以看到String 的+拼接方法的耗时了。但是使用 + 实在是方便。所以在这里建议如果字符串拼接次数在10一下,可以使用+,过多的则用StringBuffer或StringBuilder。
其它
参考:
https://blog.csdn.net/qiumengchen12/article/details/44939929
https://blog.csdn.net/chenssy/article/details/13004291
到此,本文就结束了,谢谢阅读!欢迎留言和点赞,你的支持是我写作最大的动力!
版权声明:
作者:虚无境
博客园出处:http://www.cnblogs.com/xuwujing
CSDN出处:http://blog.csdn.net/qazwsxpcm
个人博客出处:http://www.panchengming.com
原创不易,转载请标明出处,谢谢!