一、什么是注解
官方定义:
注解是一系列元数据,它提供数据用来解释程序代码,但是注解并非是所解释的代码本身的一部分。注解对于代码的运行效果没有直接影响。
注解本身不起作用,起作用的是注解解释器,注解需要和反射一起使用才能发挥大的威力。
注解有许多用处,主要如下:
提供信息给编译器:编译器可以利用注解来探测错误和警告信息。
编译阶段时的处理:软件工具可以用来利用注解信息来生成代码、Html文档或者做其它相应处理。
运行时的处理:某些注解可以在程序运行的时候接受代码的提取
值得注意的是,注解不是代码本身的一部分。
我们通俗一点理解就是:注解就相当于超市里面商品的标签,它不属于商品,它只是为商品做一些解释说明,而注解就是为我们的代码作解释说明,并不属于我们代码本身的部分。
二、注解的使用
JDK中提供的注解
JDK里面提供的几个注解
@Override: java.lang.Override 是一个标记类型注解,它被用作标注方法。它说明了被标注的方法重载了父类的方法,起到了断言的作用。如果我们使用了这种注解在一个没有覆盖父类方法的方法时,java 编译器将以一个编译错误来警示。
@Deprecated:表示该方法已经过时了。(当方法或是类上面有@Deprecated注解时,说明该方法或是类都已经过期不能再用,但不影响以前项目使用,提醒你新替代待的方法或是类。如果程序员不小心使用了它的元素,那么编译器会发出警告信息。)
@SuppressWarnings:表示忽略指定警告,比如@SuppressWarnings(“Deprecation”)
@SafeVarargs:参数安全类型注解。它的目的是提醒开发者不要用参数做一些不安全的操作,它的存在会阻止编译器产生 unchecked 这样的警告。它是在 Java 1.7 的版本中加入的。
@FunctionalInterface:函数式接口注解,这个是 Java 1.8 版本引入的新特性。
元注解
java.lang.annotation 提供了四种元注解,专门注解其他的注解(在自定义注解的时候,需要使用到元注解)。
@Retention:定义该注解的生命周期。
RetentionPolicy.SOURCE:注解只在源码阶段保留,在编译器进行编译时它将被丢弃忽视。
RetentionPolicy.CLASS:注解只被保留到编译进行的时候,它并不会被加载到 JVM 中。
RetentionPolicy.RUNTIME:注解可以保留到程序运行的时候,它会被加载进入到 JVM 中,所以在程序运行时可以获取到它们。
@Target:表示该注解用于什么地方。默认值为任何元素,表示该注解用于什么地方。可用的ElementType 参数包括:
ElementType.CONSTRUCTOR: 用于描述构造器。
ElementType.FIELD: 成员变量、对象、属性(包括enum实例)。
ElementType.LOCAL_VARIABLE: 用于描述局部变量。
ElementType.METHOD: 用于描述方法。
ElementType.PACKAGE: 用于描述包。
ElementType.PARAMETER: 用于描述参数。
ElementType.TYPE: 用于描述类、接口(包括注解类型) 或enum声明。
@Documented:一个简单的Annotations 标记注解,表示是否将注解信息添加在java 文档中。
@Inherited :表示当前注解可以由子注解来继承。
@Repeatable:是可重复的意思,Java 1.8 才加进来的,指的是注解的值可以同时取多个。
自定义注解
Annotation型定义为@interface,所有的Annotation 会自动继承java.lang.Annotation这一接口,并且不能再去继承别的类或是接口。
参数成员只能用public 或默认这两个访问权修饰,可以用default设置默认值。也可以不定义成员。
参数成员只能用基本类型byte、short、char、int、long、float、double、boolean八种基本数据类型和String、Enum、Class、annotations等数据类型,以及这一些类型的数组。
要获取类方法和字段的注解信息,必须通过Java的反射技术来获取 Annotation 对象,因为你除此之外没有别的获取注解对象的方法
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) public @interface MyAnnotation { int age() default 1; String name() default ""; } @MyAnnotation(age=26,name="bobo") public class People { }
注解经典运用
运行期利用反射可以获取注解:详情请移步吃透Java基础六:反射
注解+反射 在数据库框架方面的应用:
有一张用户表,包含id name age gender 对每一个字段进行检索并打印出Sql语句
1、自定义Table和Column注解
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) public @interface Table { String value(); } @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) public @interface Column { String value(); }
2、编写User类
@Table("user") public class User { public User(String id,String name,String age,String gender){ this.id=id; this.name=name; this.age=age; this.gender=gender; } @Column("id") private String id; @Column("name") private String name; @Column("age") private String age; @Column("gender") private String gender; public String getId() { return id; } public void setId(String id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getAge() { return age; } public void setAge(String age) { this.age = age; } public String getGender() { return gender; } public void setGender(String gender) { this.gender = gender; } }
3、利用反射获取注解信息,实现Sql语句查询
public class MyTest { public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException { User user = new User("1", "bobo", "26", "男"); System.out.println(query(user)); } public static String query(User user) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { StringBuilder stringBuilder = new StringBuilder(); Class userClass = user.getClass(); //获取表明 Annotation annotation = userClass.getAnnotation(Table.class); Table table = (Table) annotation; stringBuilder.append("select * from ").append(table.value()).append(" where 0=0"); //获取字段信息 Field[] fields = userClass.getDeclaredFields(); for (Field field : fields) { //获取列名 Column column = field.getAnnotation(Column.class); String columnName = column.value(); //获取字段值 String filedName = field.getName(); String methodName = "get" + filedName.substring(0, 1).toUpperCase() + filedName.substring(1); Method method = userClass.getMethod(methodName, null); String value = (String) method.invoke(user, null); stringBuilder.append(" and " + columnName + " = " + value); } return stringBuilder.toString(); } }
运行输出:
select * from user where 0=0 and id = 1 and name = bobo and age = 26 and gender = 男
枚举是JDK1.5添加的,在枚举类型出来之前,我们都是以定义常量来代替,比如:
public class Date { public static final int ONE = 0; public static final int TWO = 1; public static final int THREE = 2; public static final int FOUR = 3; public static final int FIVE = 4; }
这种定义常量的方式也是有一些不足的,如int值相同的情况下,编译期并不会提示我们的,所以很容易造成混淆。
定义枚举类的一般语法:任意两个枚举成员不能具有相同的名称,多个枚举成员之间使用逗号分隔,枚举表示的类型其取值是必须有限的,枚举是枚举类的实例对象,枚举成员类是jvm在运行期创建,不能在外部创建实例对象。
public enum Date { ONE,TWO,THREE,FOUR,FIVE }
把刚才定义的Date枚举类编译后再反编译class文件来看一下:
可以看到,使用关键字enum定义的枚举类型,在编译期后,也将转换成为一个实实在在的类,而在该类中,会存在每个在枚举类型中定义好变量的对应实例对象,如上述的ONE枚举类型对应public static final Date ONE;,同时编译器会为该类创建两个方法,分别是values()和valueOf()。
java.lang.Enum源码
public abstract class Enum<E extends Enum<E>> implements Comparable<E>, Serializable { //返回此枚举常量的名称,在其枚举声明中对其进行声明 private final String name; public final String name() { return name; } //返回枚举常量的序数(它在枚举声明中的位置,其中初始常量序数为零) private final int ordinal; public final int ordinal() { return ordinal; } //枚举的构造方法,只能由编译器调用 protected Enum(String name, int ordinal) { this.name = name; this.ordinal = ordinal; } //比较的是ordinal值 public final int compareTo(E o) { Enum<?> other = (Enum<?>)o; Enum<E> self = this; if (self.getClass() != other.getClass() && // optimization self.getDeclaringClass() != other.getDeclaringClass()) throw new ClassCastException(); return self.ordinal - other.ordinal;//根据ordinal值比较大小 } //返回与此枚举常量的枚举类型相对应的 Class 对象 @SuppressWarnings("unchecked") public final Class<E> getDeclaringClass() { //获取class对象引用,getClass()是Object的方法 Class<?> clazz = getClass(); //获取父类Class对象引用 Class<?> zuper = clazz.getSuperclass(); return (zuper == Enum.class) ? (Class<E>)clazz : (Class<E>)zuper; } //返回带指定名称的指定枚举类型的枚举常量 public static <T extends Enum<T>> T valueOf(Class<T> enumType, String name) { //enumType.enumConstantDirectory()获取到的是一个map集合,key值就是name值,value则是枚举变量值 //enumConstantDirectory是class对象内部的方法,根据class对象获取一个map集合的值 T result = enumType.enumConstantDirectory().get(name); if (result != null) return result; if (name == null) throw new NullPointerException("Name is null"); throw new IllegalArgumentException( "No enum constant " + enumType.getCanonicalName() + "." + name); } public String toString() { return name; } public final boolean equals(Object other) { return this==other; } public final int hashCode() { return super.hashCode(); } protected final Object clone() throws CloneNotSupportedException { throw new CloneNotSupportedException(); } protected final void finalize() { } private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { throw new InvalidObjectException("can't deserialize enum"); } private void readObjectNoData() throws ObjectStreamException { throw new InvalidObjectException("can't deserialize enum"); } }View Code
下面例子测试其方法:
public class MyTest { public static void main(String[] args) { Date[] dates = {Date.ONE, Date.TWO, Date.THREE, Date.FOUR, Date.FIVE}; for (Date date : dates) { System.out.println("name:" + date.name() + " ordinal:" + date.ordinal() + " class:" + date.getDeclaringClass()); } System.out.println("-----------------------------"); System.out.println("dates[0].compareTo(dates[1]):" + dates[0].compareTo(dates[1])); System.out.println("-----------------------------"); Date d1 = Date.valueOf(Date.class, dates[0].name()); Date d2 = Enum.valueOf(Date.class, dates[0].name()); System.out.println("d1:" + d1 + " d2:" + d2); } }
运行输出:
name:ONE ordinal0 class:class Date name:TWO ordinal1 class:class Date name:THREE ordinal2 class:class Date name:FOUR ordinal3 class:class Date name:FIVE ordinal4 class:class Date ----------------------------- dates[0].compareTo(dates[1])-1 ----------------------------- d1:ONE d2:ONE
Values()方法与ValueOf()方法
values()方法和valueOf(String name)方法是编译器生成的static方法,在Enum类中并没出现values()方法,但valueOf()方法还是有出现的,只不过编译器生成的valueOf()方法需传递一个name参数,而Enum自带的静态方法valueOf()则需要传递两个方法,其实,编译器生成的valueOf方法最终还是调用了Enum类的valueOf方法:
public class MyTest { public static void main(String[] args) { System.out.println(Arrays.toString(Date.values())); System.out.println(Date.valueOf("ONE")); } }
运行输出:
[ONE, TWO, THREE, FOUR, FIVE] ONE
values()方法的作用就是获取枚举类中的所有变量,并作为数组返回。
valueOf(String name)方法与Enum类中的valueOf方法的作用类似根据名称获取枚举变量。
三:枚举类使用
enum 与 class、interface 具有相同地位,但是不能继承与被继承,也不能创建实例,只能由JVM去创建。
可以实现多个接口,也可以拥有构造器、成员方法、成员变量。
实现接口
public interface ClickListener { void onClick(); } public enum Date implements ClickListener{ ONE, TWO, THREE, FOUR, FIVE; @Override public void onClick() { } }
定义构造函数
注意:只能是私有的构造函数或默认
public enum Date{ ONE, TWO, THREE, FOUR, FIVE; private Date(){} }
定义成员方法和成员变量
public enum Date{ ONE, TWO, THREE, FOUR, FIVE; private String a; public String getContext(){ return Arrays.toString(values()); } }
EnumMap
java.util.EnumSet和java.util.EnumMap是两个枚举集合。EnumMap继承了AbstractMap类,因此EnumMap具备一般map的使用方法,主要通过一个保存key的数组K[] keyUniverse和一个保存value的数组Object[] vals来操作。
源码:
public class EnumMap<K extends Enum<K>, V> extends AbstractMap<K, V> implements java.io.Serializable, Cloneable { //Class对象引用 private final Class<K> keyType; //存储Key值的数组 private transient K[] keyUniverse; //存储Value值的数组 private transient Object[] vals; //map的size private transient int size = 0; //空map private static final Enum<?>[] ZERO_LENGTH_ENUM_ARRAY = new Enum<?>[0]; //构造函数 public EnumMap(Class<K> keyType) { this.keyType = keyType; keyUniverse = getKeyUniverse(keyType); vals = new Object[keyUniverse.length]; } //返回枚举数组 private static <K extends Enum<K>> K[] getKeyUniverse(Class<K> keyType) { //最终调用到枚举类型的values方法,values方法返回所有可能的枚举值 return SharedSecrets.getJavaLangAccess().getEnumConstantsShared(keyType); } public V put(K key, V value) { typeCheck(key);//检测key的类型 //获取存放value值得数组下标 int index = key.ordinal(); //获取旧值 Object oldValue = vals[index]; //设置value值 vals[index] = maskNull(value); if (oldValue == null) size++; return unmaskNull(oldValue);//返回旧值 } private void typeCheck(K key) { Class<?> keyClass = key.getClass();//获取类型信息 if (keyClass != keyType && keyClass.getSuperclass() != keyType) throw new ClassCastException(keyClass + " != " + keyType); } //代表NULL值得空对象实例 private static final Object NULL = new Object() { public int hashCode() { return 0; } public String toString() { return "java.util.EnumMap.NULL"; } }; private Object maskNull(Object value) { //如果值为空,返回NULL对象,否则返回value return (value == null ? NULL : value); } @SuppressWarnings("unchecked") private V unmaskNull(Object value) { //将NULL对象转换为null值 return (V)(value == NULL ? null : value); } public V get(Object key) { return (isValidKey(key) ? unmaskNull(vals[((Enum<?>)key).ordinal()]) : null); } //对Key值的有效性和类型信息进行判断 private boolean isValidKey(Object key) { if (key == null) return false; // Cheaper than instanceof Enum followed by getDeclaringClass Class<?> keyClass = key.getClass(); return keyClass == keyType || keyClass.getSuperclass() == keyType; } public V remove(Object key) { //判断key值是否有效 if (!isValidKey(key)) return null; //直接获取索引 int index = ((Enum<?>)key).ordinal(); Object oldValue = vals[index]; //对应下标元素值设置为null vals[index] = null; if (oldValue != null) size--;//减size return unmaskNull(oldValue); } //判断是否包含某value public boolean containsValue(Object value) { value = maskNull(value); //遍历数组实现 for (Object val : vals) if (value.equals(val)) return true; return false; } //判断是否包含key public boolean containsKey(Object key) { return isValidKey(key) && vals[((Enum<?>)key).ordinal()] != null; } }View Code
下面例子测试其方法:
public class MyTest { public static void main(String[] args) { EnumMap enumMap=new EnumMap(Date.class); //添加元素 enumMap.put(Date.ONE,1); enumMap.put(Date.TWO,2); enumMap.put(Date.THREE,3); //删除元素 enumMap.remove(Date.TWO); //遍历输出 System.out.println(enumMap.toString()); } }
运行输出:
{ONE=1, THREE=3}
public abstract class EnumSet<E extends Enum<E>> extends AbstractSet<E> implements Cloneable, java.io.Serializable { //元素的class final Class<E> elementType; final Enum<?>[] universe; private static Enum<?>[] ZERO_LENGTH_ENUM_ARRAY = new Enum<?>[0]; EnumSet(Class<E>elementType, Enum<?>[] universe) { this.elementType = elementType; this.universe = universe; } //创建一个具有指定元素类型的空EnumSet。 public static <E extends Enum<E>> EnumSet<E> noneOf(Class<E> elementType) { Enum<?>[] universe = getUniverse(elementType); if (universe == null) throw new ClassCastException(elementType + " not an enum"); if (universe.length <= 64) return new RegularEnumSet<>(elementType, universe); else return new JumboEnumSet<>(elementType, universe); } //创建一个指定元素类型并包含所有枚举值的EnumSet public static <E extends Enum<E>> EnumSet<E> allOf(Class<E> elementType) { EnumSet<E> result = noneOf(elementType); result.addAll(); return result; } abstract void addAll(); 创建一个包含参数容器中的所有元素的EnumSet public static <E extends Enum<E>> EnumSet<E> copyOf(EnumSet<E> s) { return s.clone(); } public static <E extends Enum<E>> EnumSet<E> copyOf(Collection<E> c) { if (c instanceof EnumSet) { return ((EnumSet<E>)c).clone(); } else { if (c.isEmpty()) throw new IllegalArgumentException("Collection is empty"); Iterator<E> i = c.iterator(); E first = i.next(); EnumSet<E> result = EnumSet.of(first); while (i.hasNext()) result.add(i.next()); return result; } } // 初始集合包括指定集合的补集 public static <E extends Enum<E>> EnumSet<E> complementOf(EnumSet<E> s) { EnumSet<E> result = copyOf(s); result.complement(); return result; } // 创建一个包括参数中所有元素的EnumSet public static <E extends Enum<E>> EnumSet<E> of(E e) { EnumSet<E> result = noneOf(e.getDeclaringClass()); result.add(e); return result; } public static <E extends Enum<E>> EnumSet<E> of(E e1, E e2) { EnumSet<E> result = noneOf(e1.getDeclaringClass()); result.add(e1); result.add(e2); return result; } public static <E extends Enum<E>> EnumSet<E> of(E e1, E e2, E e3) { EnumSet<E> result = noneOf(e1.getDeclaringClass()); result.add(e1); result.add(e2); result.add(e3); return result; } public static <E extends Enum<E>> EnumSet<E> of(E e1, E e2, E e3, E e4) { EnumSet<E> result = noneOf(e1.getDeclaringClass()); result.add(e1); result.add(e2); result.add(e3); result.add(e4); return result; } public static <E extends Enum<E>> EnumSet<E> of(E e1, E e2, E e3, E e4, E e5) { EnumSet<E> result = noneOf(e1.getDeclaringClass()); result.add(e1); result.add(e2); result.add(e3); result.add(e4); result.add(e5); return result; } @SafeVarargs public static <E extends Enum<E>> EnumSet<E> of(E first, E... rest) { EnumSet<E> result = noneOf(first.getDeclaringClass()); result.add(first); for (E e : rest) result.add(e); return result; } // 创建一个包括枚举值中指定范围元素的EnumSet public static <E extends Enum<E>> EnumSet<E> range(E from, E to) { if (from.compareTo(to) > 0) throw new IllegalArgumentException(from + " > " + to); EnumSet<E> result = noneOf(from.getDeclaringClass()); result.addRange(from, to); return result; } abstract void addRange(E from, E to); @SuppressWarnings("unchecked") public EnumSet<E> clone() { try { return (EnumSet<E>) super.clone(); } catch(CloneNotSupportedException e) { throw new AssertionError(e); } } abstract void complement(); final void typeCheck(E e) { Class<?> eClass = e.getClass(); if (eClass != elementType && eClass.getSuperclass() != elementType) throw new ClassCastException(eClass + " != " + elementType); } private static <E extends Enum<E>> E[] getUniverse(Class<E> elementType) { return SharedSecrets.getJavaLangAccess() .getEnumConstantsShared(elementType); } }View Code
EnumSet
EnumSet是与枚举类型一起使用的专用 Set 集合,EnumSet 中所有元素都必须是枚举类型。注意EnumSet不允许使用 null 元素。试图插入 null 元素将抛出 NullPointerException,但试图测试判断是否存在null 元素或移除 null 元素则不会抛出异常,与大多数collection 实现一样,EnumSet不是线程安全的,因此在多线程环境下应该注意数据同步问题。
下面例子测试其方法:
创建EnumSet并不能使用new关键字,因为它是个抽象类,而应该使用其提供的静态工厂方法,EnumSet的静态工厂方法比较多,如下
public class MyTest { public static void main(String[] args) { EnumSet<Date> enumSet= EnumSet.noneOf(Date.class); enumSet.add(Date.ONE); enumSet.add(Date.TWO); enumSet.add(Date.THREE); System.out.println("添加后:"+enumSet.toString()); EnumSet<Date> enumSet1= EnumSet.allOf(Date.class); System.out.println("add后:"+enumSet1.toString()); //初始集合包括枚举值中指定范围的元素 EnumSet<Date> enumSet2= EnumSet.range(Date.ONE,Date.THREE); System.out.println("指定范围:"+enumSet2.toString()); //指定补集,也就是从全部枚举类型中去除参数集合中的元素,如下去掉上述enumSet2的元素 EnumSet<Date> enumSet3= EnumSet.complementOf(enumSet2); System.out.println("指定补集:"+enumSet3.toString()); //初始化时直接指定元素 EnumSet<Date> enumSet4= EnumSet.of(Date.ONE); System.out.println("指定Date.ONE元素:"+enumSet4.toString()); EnumSet<Date> enumSet5= EnumSet.of(Date.ONE,Date.FOUR); System.out.println("指定Color.BLACK和Color.GREEN元素:"+enumSet5.toString()); //复制enumSet5容器的数据作为初始化数据 EnumSet<Date> enumSet6= EnumSet.copyOf(enumSet5); System.out.println("enumSet6:"+enumSet6.toString()); List<Date> list = new ArrayList<Date>(); list.add(Date.ONE); list.add(Date.ONE);//重复元素 list.add(Date.TWO); list.add(Date.THREE); System.out.println("list:"+list.toString()); //使用copyOf(Collection<E> c) EnumSet enumSet7=EnumSet.copyOf(list); System.out.println("enumSet7:"+enumSet7.toString()); } }
运行输出:
添加后:[ONE, TWO, THREE] add后:[ONE, TWO, THREE, FOUR, FIVE] 指定范围:[ONE, TWO, THREE] 指定补集:[FOUR, FIVE] 指定Date.ONE元素:[ONE] 指定Color.BLACK和Color.GREEN元素:[ONE, FOUR] enumSet6:[ONE, FOUR] list:[ONE, ONE, TWO, THREE] enumSet7:[ONE, TWO, THREE]
1、创建字符串方式
2、String类是不可变的
public final class String implements java.io.Serializable, Comparable<String>, CharSequence { /** The value is used for character storage. */ private final char value[]; ... }
String类被final关键字修饰,意味着String类不能被继承,并且它的成员方法都默认为final方法;字符串一旦创建就不能再修改。
String实例的值是通过字符数组实现字符串存储的。
String类不可变的好处?
作为HashMap的键:因为String是不可变的,当创建String的时候哈希吗已经计算好了,所以每次在使用字符串的哈希码的时候就不用再计算一次,更高效。
线程安全:因为字符串是不可变的,所以一定是线程安全的,不用考虑多线程访问加锁的情况。
字符串常量池的需要。
3、String类常用方法
二、String高级
1、字符串常量池
字符串的分配和其他对象分配一样,是需要消耗高昂的时间和空间的,而且字符串使用的非常多。JVM为了提高性能和减少内存的开销,在实例化字符串的时候进行了一些优化:使用字符串常量池。每当创建字符串常量时,JVM会首先检查字符串常量池,如果该字符串已经存在常量池中,那么就直接返回常量池中的实例引用。如果字符串不存在常量池中,就会实例化该字符串并且将其放到常量池中。由于String字符串的不可变性,常量池中一定不存在两个相同的字符串。
字符串常量池在jdk1.7之前是存在方法去的,从1.7之后放到堆里面了。
2、String test = "abc"和new String(“abc”)区别
String test = “abc”:首先检查字符串常量池中是否存在此字符串,如果存在则直接返回字符串常量池中字符串的引用,如果不存在则添加此字符串进常量池然后返回此引用。
new String(“abc”):直接在堆中创建字符串返回新创建字符串的引用,每次创建都是一个新的对象。
如下例子:
String str = "bobo"; String str1 = "bobo"; String str2 = new String("bobo"); String str3 = new String("bobo"); System.out.println(str == str1);//true System.out.println(str == str2);//false System.out.println(str2 == str3);//false
3、intern()方法
直接使用双引号创建出来的String对象会直接存储在字符串常量池中,new创建的的String对象,可以使用String提供的intern方法。intern 方法是一个native方法,intern方法会从字符串常量池中查询当前字符串是否存在,如果存在,就直接返回当前字符串;如果不存在就会将当前字符串放入常量池中,之后再返回。
如下例子:
String str = "bobo"; String str2 = new String("bobo").intern(); System.out.println(str == str2);//true
4、+字符串拼接
使用“+”拼接字符串时,JVM会隐式创建StringBuilder对象,每一个拼接就会隐式创建一个StringBuilder对象,当大量字符串拼接时,就会有大量StringBuilder在堆内存中创建,肯定会造成效率的损失,所以一般大量字符串拼接建议用StringBuilder(线程不安全)或StringBuffer(线程安全)。
如下例子看一下两者效率:
public class MyTest { public static void main(String[] args) { int count = 100000; long startTime = System.currentTimeMillis(); String result = ""; for (int i = 0; i < count; i++) { result += "bobo"; } long endTime = System.currentTimeMillis(); System.out.println(endTime - startTime); long startTime1 = System.currentTimeMillis(); StringBuilder stringBuilder = new StringBuilder(); for (int i = 0; i < count; i++) { stringBuilder.append("bobo"); } long endTime1 = System.currentTimeMillis(); System.out.println(endTime1 - startTime1); } }
String字符串直接相加不会在堆中创建临时对象,效率高,就不用使用stringbuffer,否则大部分情况就用后者,
5、StringBuilder和StringBuffer拼接字符串
StringBuilder为了解决String使用”+“大量拼接字符串时产生很多中间对象问题而提供的一个类,提供append和add方法,可以将字符串添加到已有序列的末尾或指定位置,实际上底层都是利用可修改的char数组(JDK 9 以后是 byte数组)来存储的,当前容量不足时触发扩容机制。
StringBuffer和StringBuilder的机制一样,唯一不同点是StringBuffer内部使用synchronized关键字来保证线程安全,保证线程安全不可避免的产生了一些额外的开销,如果不要求线程安全的情况下还是建议优先使用StringBuilder。
StringBuilder和StringBuffer都是继承AbstractStringBuilder抽象类,里面实现了字符串拼接的具体逻辑,我们来看一下:
abstract class AbstractStringBuilder implements Appendable, CharSequence { /** * The value is used for character storage. */ char[] value; ... }
数据都是保存在char[]里面,当容量不足时进行数组的扩容:
private void ensureCapacityInternal(int minimumCapacity) { // overflow-conscious code if (minimumCapacity - value.length > 0) { value = Arrays.copyOf(value, newCapacity(minimumCapacity)); } } private int newCapacity(int minCapacity) { // overflow-conscious code int newCapacity = (value.length << 1) + 2; if (newCapacity - minCapacity < 0) { newCapacity = minCapacity; } return (newCapacity <= 0 || MAX_ARRAY_SIZE - newCapacity < 0) ? hugeCapacity(minCapacity) : newCapacity; } private int hugeCapacity(int minCapacity) { if (Integer.MAX_VALUE - minCapacity < 0) { // overflow throw new OutOfMemoryError(); } return (minCapacity > MAX_ARRAY_SIZE) ? minCapacity : MAX_ARRAY_SIZE; }
从源码可以看到,当需要容量不足时触发扩容机制,扩容为原来的2倍+2的容量,最大数组扩容容量为:MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8,Integer.MAX_VALUE为2的31次方-1。扩容完后把老的数组里面的数据拷贝到新的数组里面。
6、String字符串长度限制
要回答这个问题要分两个阶段看,分别是编译期和运行期。不同的时期限制不一样。
编译期
我们所能想到的是String源码中定义,存储String字符的char数组最大是Integer.MAX_VALUE为2的31次方-1,那么也就是说可以存储2147483647个字符,分析到这时是不是以为大功告成了,其实不然,我们忽略了一点。
我们编译java文件为class文件的时候是把字符串放在class的常量池中的,JVM对常量池的容量有严格的限制,JVN规定,字符串是由CONSTANTUtf8info类型的结构,CONSTANTUtf8_info的定义如下:
CONSTANT_Utf8_info{ u1 tag; u2 length; u1 bytes; }
其中u1、u2是无符号的分别代表1个字节和2个字节,那么我们只需要看length最多允许两个字节的长度,因此理论上允许的的最大长度是2^16=65536。而 java class 文件是使用一种变体UTF-8格式来存放字符的,null 值使用两个 字节来表示,因此只剩下 65536- 2 = 65534个字节。
所以,在Java中,所有需要保存在常量池中的数据,长度最大不能超过65535,这当然也包括字符串的定义。
运行期
上面提到的这种String长度的限制是编译期的限制,也就是使用String str= “”;这种字面值方式定义的时候才会有的限制。
String在运行期的限制,其实就是我们前文提到的那个Integer.MAX_VALUE ,这个值约等于4G,在运行期,如果String的长度超过这个范围,就会抛出异常。
1、何为构造方法
构造方法是类的一种特殊方法,用来初始化类的一个新的对象,每个类至少有一个构造方法,如果类中没有显式定义构造方法,编译器在编译的时候会生成一个默认的构造方法,默认的构造方法不包含任何参数,并且方法体为空。如果类中显式地定义了一个或多个构造方法,则 Java 不再提供默认构造方法。
构造方法必须具有和类名相同的名称,而且没有返回类型。构造方法的默认返回类型就是对象类型本身,并且构造方法不能被 static、final、synchronized、abstract 和 native 修饰。
在一个类中,与类名相同的方法就是构造方法。每个类可以具有多个构造方法,但要求它们各自包含不同的方法参数。
构造方法可以用public、protected、默认、private修饰。
2、子类构造方法调用父类构造方法
子类构造方法要么调用父类无参构造方法(包括当父类没有构造方法时。系统默认给的无参构造方法),要么调用父类有参构造方法。当子类构造方法调用父类无参构造方法,一般都是默认不写的,要写的话就是super(),且要放在构造方法的第一句。当子类构造方法要调用父类有参数的构造方法,那么子类的构造方法中必须要用super(参数)调用父类构造方法,且要放在构造方法的第一句。
看完如下例子对子类父类构造函数调用顺序应该就能明白了:
public class Father { private String name; public Father() { System.out.println("父类无参构造方法"); } public Father(String name) { System.out.println("父类有参构造方法 name:" + name); } } public class Son extends Father { public Son() { System.out.println("子类无参构造函数"); } public Son(String name) { super(name); System.out.println("子类有参构造函数"); } public Son(String name, int age) { System.out.println("子类有参构造函数 name and age"); } public static void main(String[] args) { new Son(); System.out.println("------------参数-----------"); new Son("bobo"); System.out.println("------------参数 name and age-----------"); new Son("bobo", 26); } }
父类无参构造方法 子类无参构造函数 ------------参数----------- 父类有参构造方法 name:bobo 子类有参构造函数 ------------参数 name and age----------- 父类无参构造方法 子类有参构造函数 name and age
Java中代码块分为普通代码块和静态代码块,普通代码块是属于实例对象的,其实在编译期编译器会把普通代码块的内容放在构造函数里面来执行的。静态代码块是属于类的,是随着类的初始化而调用的,那么类是什么时候初始化呢?请参考之前写的吃透Java基础三:触发类初始化的五种方式
1、普通代码块编译后自动添加到构造函数最上面执行
看下面例子:
public class Son { private String name; { System.out.println("普通代码块"); } public Son() { System.out.println("子类无参构造函数"); } public Son(String name) { this.name = name; System.out.println("子类有参构造函数"); } }
编译后的class文件反编译:
public class Son { private String name; public Son() { System.out.println("普通代码块"); System.out.println("子类无参构造函数"); } public Son(String name) { System.out.println("普通代码块"); this.name = name; System.out.println("子类有参构造函数"); } }
2、有继承关系的静态代码块和普通代码块调用顺序
只需要记住一点:静态代码块是在类初始化的时候调用的,普通代码块是在创建对象实例的时候在构造函数里面调用的,类初始化一定在创建对象实例之前,所以静态代码块一定在普通代码块之前调用。
看如下例子:
public class Father { static { System.out.println("父类静态代码块"); } { System.out.println("父类普通代码块"); } public Father() { System.out.println("父类无参构造方法"); } } public class Son extends Father{ { System.out.println("子类普通代码块"); } static { System.out.println("子类静态代码块"); } public Son() { System.out.println("子类构造函数"); } public static void main(String[] args) { new Son(); } }
父类静态代码块 子类静态代码块 父类普通代码块 父类无参构造方法 子类普通代码块 子类构造函数
三、方法的重载与重写
方法的重写(Overriding)和重载(Overloading)是java多态性的不同表现,重写是父类与子类之间多态性的一种表现,重载可以理解成多态的具体表现形式。
1、重载
重载(overloading) 是在一个类里面,方法名字相同,而参数不同。返回类型可以相同也可以不同。每个重载的方法(或者构造函数)都必须有一个独一无二的参数类型列表。最常用的地方就是构造器的重载。
规则:
综上:参数列表区分重载
public class MyTest { public int test(){ System.out.println("test1"); return 1; } public void test(int a){ System.out.println("test2"); } //以下两个参数类型顺序不同 public String test(int a,String s){ System.out.println("test3"); return "test3"; } public String test(String s,int a){ System.out.println("test4"); return "test4"; } }
2、重写
重写是子类对父类的允许访问的方法的实现过程进行重新编写, 方法名和形参都不能改变。子类重写父类方法的规则:
子类重写的方法的方法名和形参列表与父类被重写的方法的方法名和形参列表相同。
子类重写的方法的权限修饰符不小于父类被重写的方法的权限修饰符,特殊情况:子类不能重写父类被声明为private权限的方法。
返回值类型:
1、父类是void,子类只能是void。
2、父类是A类型,子类是A类或A类的子类。
3、父类是基本数据类型(如:int),子类返回值类型必须相同。
子类重写的方法抛出的异常的类型不大于父类被重写的方法抛出的异常类型。
如下例子:
public class Father { public void overrideMethod1(){ } public int overrideMethod2() throws IOException { return 0; } protected List<String> overrideMethod3(){ return null; } } public class Son extends Father{ /** * 父类方法返回值是void,子类重写此方法返回值只能是void */ @Override public void overrideMethod1() { } /** * 1、父类方法访问权限是protected,子类重写时访问权限可以是public。 * 2、父类方法抛出的异常是IOException,子类重写方法时抛出的异常可以是其子类FileNotFoundException。 * 3、父类方法返回值是int基本数据类型,子类重写时返回值类型只能是int。 * @return * @throws FileNotFoundException */ @Override public int overrideMethod2() throws FileNotFoundException { return 0; } /** * 父类方法返回值类型是List,子类重写方法返回值类型可以是其子类ArrayList。 * @return */ @Override public ArrayList<String> overrideMethod3() { return null; } }