前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >一文读懂字符串String

一文读懂字符串String

作者头像
编程识堂
发布2023-05-24 14:47:53
4860
发布2023-05-24 14:47:53
举报
文章被收录于专栏:编程识堂编程识堂

前言

String在java中特别常用,但小伙伴儿们对String真的彻底了解了吗?今天跟着小堂我一起盘它、弄懂它。

为什么说字符串是不可变的

在工作中,我们经常要在代码中对字符串进行赋值和改变它的值,但是,为什么我们说字符串是不可变的呢?

源码解析

代码语言:javascript
复制
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final char value[];

     /**
     * Initializes a newly created {@code String} object so that it represents
     * an empty character sequence.  Note that use of this constructor is
     * unnecessary since Strings are immutable.
     */
    public String() {
        this.value = "".value;
    }

    /**
     * Initializes a newly created {@code String} object so that it represents
     * the same sequence of characters as the argument; in other words, the
     * newly created string is a copy of the argument string. Unless an
     * explicit copy of {@code original} is needed, use of this constructor is
     * unnecessary since Strings are immutable.
     *
     * @param  original
     *         A {@code String}
     */
    public String(String original) {
        this.value = original.value;
        this.hash = original.hash;
    }
}

String类的值是保存在value数组中的,并且是被private final修饰的

  1. private修饰,表明外部的类是访问不到value的,同时⼦类也访问不到,当然String类不可能有⼦ 类,因为类被final修饰了。
  2. final修饰,表明value的引⽤是不会被改变的,⽽value只会在String的构造函数中被初始化,⽽且 并没有其他⽅法可以修改value数组中的值,保证了value的引⽤和值都不会发⽣变化。

final关键字的作⽤有如下⼏种:

  1. final修饰类时,表明这个类不能被继承
  2. final修饰⽅法,表明⽅法不能被重写
  3. final修饰变量,如果是基本数据类型的变量,则其数值⼀旦在初始化之后便不能改变;如果是对象 类型的变量,只能保证它的引⽤不变,但对象的内容是可以改变的

在Java中数组也是对象,数组即使被final修饰,内容还是可以改变的。 所以我们说String类是不可变的。⽽很多⽅法,如substring并不是在原来的String类上进⾏操作,⽽是⽣成了新的String类。

代码语言:javascript
复制
public String substring(int beginIndex) {
 if (beginIndex < 0) {
 throw new StringIndexOutOfBoundsException(beginIndex);
 }
 int subLen = value.length - beginIndex;
 if (subLen < 0) {
 throw new StringIndexOutOfBoundsException(subLen);
 }
 return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
}

什么是不可变对象

首先,我们需要知道什么是不可变对象?

不可变对象是在完全创建后其内部状态保持不变的对象。这意味着,一旦对象被赋值给变量,我们既不能更新引用,也不能通过任何方式改变内部状态。

原因

可是有人会有疑惑,String为什么不可变,我的代码中经常改变String的值啊,如下:

代码语言:javascript
复制
String s = "abcd";
s = s.concat("ef");

这样,操作,不就将原本的"abcd"的字符串改变成"abcdef"了么?

但是,虽然字符串内容看上去从"abcd"变成了"abcdef",但是实际上,我们得到的已经是一个新的字符串了。

如上图,在堆中重新创建了一个"abcdef"字符串,和"abcd"并不是同一个对象。

所以,一旦一个string对象在内存(堆)中被创建出来,他就无法被修改。而且,String类的所有方法都没有改变字符串本身的值,都是返回了一个新的对象。

如果我们想要一个可修改的字符串,可以选择StringBuffer 或者 StringBuilder这两个代替String。

为什么String要设计成不可变

在知道了"String是不可变"的之后,大家是不是一定都很疑惑:为什么要把String设计成不可变的呢?有什么好处呢?

这个问题,困扰过很多人,甚至有人直接问过Java的创始人James Gosling。

在一次采访中James Gosling被问到什么时候应该使用不可变变量,他给出的回答是:

I would use an immutable whenever I can.

那么,他给出这个答案背后的原因是什么呢?是基于哪些思考的呢?

其实,主要是从缓存、安全性、线程安全和性能等角度触发的。

Q:缓存、安全性、线程安全和性能?这又都是啥 A:你别急,听我一个一个给你讲就好了。

缓存

字符串是使用最广泛的数据结构。大量的字符串的创建是非常耗费资源的,所以,Java提供了对字符串的缓存功能,可以大大地节省堆空间。

JVM中专门开辟了一部分空间来存储Java字符串,那就是字符串常量池。

通过字符串常量池,两个内容相同的字符串变量,可以从池中指向同一个字符串对象,从而节省了关键的内存资源。

代码语言:javascript
复制
String s = "abcd";
String s2 = s;

对于这个例子,s和s2都表示"abcd",所以他们会指向字符串池中的同一个字符串对象:

但是,之所以可以这么做,主要是因为字符串的不变性。试想一下,如果字符串是可变的,我们一旦修改了s的内容,那必然导致s2的内容也被动地改变了,这显然不是我们想看到的。

面试的时候我们经常被问到:

代码语言:javascript
复制
String str1 = "abc";
String str2 = "abc";
String str3 = new String("abc");
String str4 = new String("abc");
// true
System.out.println(str1 == str2);
// false
System.out.println(str1 == str3);
// false
System.out.println(str3 == str4);

内存中的结构如下:

其中常量池中存的是引⽤

解释⼀下上⾯代码的输出,Java中有2种创建字符串对象的⽅式

代码语言:javascript
复制
String str1 = "abc"; 
String str2 = "abc"; 
// true 
System.out.println(str1 == str2);

采⽤字⾯值的⽅式创建⼀个字符串时,JVM⾸先会去字符串池中查找是否存在"abc"这个对象的引⽤

如果不存在,则在堆中创建"abc"这个对象,并将其引⽤添加到字符串常量池(实际上是将引⽤放到哈希

表中),随后将引⽤赋给str1

如果存在,则不创建任何对象,直接将池中"abc"对象的引⽤返回,赋给str2。因为str1、str2指向同⼀个对象,所以结果为true。

代码语言:javascript
复制
String str3 = new String("abc"); 
String str4 = new String("abc"); 
// false 
System.out.println(str3 == str4);

采⽤new关键字新建⼀个字符串对象时,JVM⾸先在字符串池中查找有没有"abc"这个字符串对象的引⽤,

如果没有,则先在堆中创建⼀个"abc"字符串对象,并将引⽤添加到字符串常量池,随后将引⽤赋给str3。

如果有,则不往池中放"abc"对象的引⽤,直接在堆中创建⼀个"abc"字符串对象,然后将引⽤赋给 str4。这样,str4就指向了堆中创建的这个"abc"字符串对象;

因为str3和str4指向的是不同的字符串对象,结果为false。

安全性

字符串在Java应用程序中广泛用于存储敏感信息,如用户名、密码、连接url、网络连接等。JVM类加载器在加载类的时也广泛地使用它。

因此,保护String类对于提升整个应用程序的安全性至关重要。

当我们在程序中传递一个字符串的时候,如果这个字符串的内容是不可变的,那么我们就可以相信这个字符串中的内容。

但是,如果是可变的,那么这个字符串内容就可能随时都被修改。那么这个字符串内容就完全不可信了。这样整个系统就没有安全性可言了。

线程安全

不可变会自动使字符串成为线程安全的,因为当从多个线程访问它们时,它们不会被更改。

因此,一般来说,不可变对象可以在同时运行的多个线程之间共享。它们也是线程安全的,因为如果线程更改了值,那么将在字符串池中创建一个新的字符串,而不是修改相同的值,不会发生竞争条件,也不需要进行额外的同步操作。因此,字符串对于多线程来说是安全的。

hashcode缓存

由于字符串对象被广泛地用作数据结构,它们也被广泛地用于哈希实现,如HashMap、HashTable、HashSet等。在对这些散列实现进行操作时,经常调用hashCode()方法。

不可变性保证了字符串的值不会改变。因此,hashCode()方法在String类中被重写,以方便缓存,这样在第一次hashCode()调用期间计算和缓存散列,并从那时起返回相同的值。

在String类中,有以下代码:

代码语言:javascript
复制
private int hash;//this is used to cache hash code.

性能

前面提到了的字符串池、hashcode缓存等,都是提升性能的提现。

因为字符串不可变,所以可以用字符串池缓存,可以大大节省堆内存。而且还可以提前对hashcode进行缓存,更加高效

由于字符串是应用最广泛的数据结构,提高字符串的性能对提高整个应用程序的总体性能有相当大的影响。

字符串拼接

字符串拼接是我们在Java代码中比较经常要做的事情,就是把多个字符串拼接到一起。

我们都知道,String是Java中一个不可变的类,所以它一旦被实例化就无法被修改。但是,既然字符串是不可变的,那么字符串拼接又是怎么回事呢?

字符串不变性与字符串拼接

其实,所有的所谓字符串拼接,都是重新生成了一个新的字符串。下面一段字符串拼接代码:

代码语言:javascript
复制
String s = "abcd";
s = s.concat("ef");

其实我们最后得到的是一个新字符串,如下图:

s保存的是一个重新创建出来的String对象的引用。

那么我们在工作过程中会遇到很多种拼接方式,小伙伴儿们,平时是怎么使用的呢?今天跟着小堂来一起回顾下比较常用的方式。

使用+拼接字符串

演示

拼接字符串最简单的方式就是直接使用+号拼接,如:

代码语言:javascript
复制
String wechat = "编程识堂";
String introduce = "每日更新Java相关技术文章,关注我不迷路";
String bcst = wechat + "," + introduce;
System.out.println(bcst);
//结果为:编程识堂,每日更新Java相关技术文章,关注我不迷路

原理

通过反编译成字节码后我们发现,主要是通过StringBuilder的append方法实现的。

concat

演示

使用String类中的concat方法来拼接字符串。如:

代码语言:javascript
复制
String wechat = "编程识堂";
String introduce = "每日更新Java相关技术文章,关注我不迷路";
System.out.println(wechat.concat(introduce));

原理

源码
代码语言:javascript
复制
wechat.concat(introduce);
/**
str="每日更新Java相关技术文章,关注我不迷路"; otherLen=21
this:wechat =  "编程识堂";
value: "编程识堂"  length:4;

**/
public String concat(String str) {
        if (str.isEmpty()) {
            return this;
        }
        int len = value.length;
        int otherLen = str.length();
        char buf[] = Arrays.copyOf(value, len + otherLen);
        str.getChars(buf, len);
        return new String(buf, true);
    }

/**
 Arrays.copyOf 方法 
 original:{编,程,识,堂} 源数组
 newLength: 25
**/
 public static char[] copyOf(char[] original, int newLength) {
        char[] copy = new char[newLength];
      /**
      original:{编,程,识,堂} 源数组
      srcPos:0 从源数组中0位置开始复制元素到目标数组中
      copy:{,,......,,,} length=25 目标数组
      destPos:0 目标数组中从0位置开始存储源数组的中的元素
      Math.min(original.length4, newLength) 要复制的元素个数 original.length=4个
      **/
        System.arraycopy(original, 0, copy, 0,
                         Math.min(original.length, newLength));
        /**
        copy:{'编','程','识','堂','\u0000',......,'\u0000'} length=25
        **/
        return copy;
 }

 /**    str.getChars方法
       dst[] 目标数组:{'编','程','识','堂','\u0000',......,'\u0000'} length=25
       dstBegin: 4 目标数组中从4位置开始存储源数组的中的元素
     * Copy characters from this string into dst starting at dstBegin.
     * This method doesn't perform any range checking.
     */
 void getChars(char dst[], int dstBegin) {
     /**
       value:源数组:{'每','日','更','新','J','a','v','a','相','关','技','术',
       '文','章',',','关','注','我','不','迷','路'}
       value.length:21
      **/
      System.arraycopy(value, 0, dst, dstBegin, value.length);
 }
System.arraycopy

System.arraycopy是Java语言中一个用于数组复制的方法。它可以将一个数组的部分或全部元素复制到另一个数组中。

方法签名:public static void arraycopy(Object src, int srcPos, Object dest, int destPos, int length)

参数解释:

  • src:源数组
  • srcPos:源数组中复制元素的起始位置
  • dest:目标数组
  • destPos:目标数组中复制元素的起始位置
  • length:要复制的元素个数

使用示例:

代码语言:javascript
复制
int[] src = {1, 2, 3, 4, 5}; 
int[] dest = new int[5]; 
System.arraycopy(src, 0, dest, 0, 5);
//结果 dest:{1, 2, 3, 4, 5}

上述代码将src数组中的所有元素复制到dest数组中,两个数组中的元素值相同。

StringBuffer和StringBuilder

演示

关于字符串,Java中除了定义了一个可以用来定义字符串常量的String类以外,还提供了可以用来定义字符串变量的StringBuffer和StringBuilder类,它的对象是可以扩充和修改的。

使用StringBuffer可以方便地对字符串进行拼接。如:

代码语言:javascript
复制
 StringBuffer sb = new StringBuffer("编程识堂");
 sb.append("每日更新Java相关技术文章,关注我不迷路");
 System.out.println(sb.toString());
//编程识堂每日更新Java相关技术文章,关注我不迷路

原理

根据类图,我们知道StringBuffer和StringBuilder都继承⾃AbstractStringBuilder类。

源码

AbstractStringBuilder

代码语言:javascript
复制
abstract class AbstractStringBuilder implements Appendable, CharSequence {
    /**
     * The value is used for character storage.
     */
    char[] value;

    /**
     * The count is the number of characters used.
    与String不同,字符数组中不一定所有位置都已经被使用,它有一个实例变量,表示数组中
   已经使用的字符个数,定义如下    
     */
    int count;

    /**
     * This no-arg constructor is necessary for serialization of subclasses.
     */
    AbstractStringBuilder() {
    }


   public AbstractStringBuilder append(String str) {
        if (str == null)
            return appendNull();
        int len = str.length();
        //⾃动扩容机制
        ensureCapacityInternal(count + len);
        str.getChars(0, len, value, count);
        count += len;
        return this;
    }
}

看到区别了吗?value数组没有⽤private和final修饰,说明了StringBuffer和StringBuilder是可变的。

与String不同,字符数组中不一定所有位置都已经被使用,它有一个实例变量,表示数组中已经使用的字符个数(count)。

抽象类AbstractStringBuilder内部提供了⼀个⾃动扩容机制,当发现⻓度不够的时候,会⾃动进⾏

扩容⼯作(具体扩容可以看源码,很容易理解),会创建⼀个新的数组,并将原来数组的数据复制

到新数组,不会创建新对象,拼接字符串的效率⾼.

StringBuilder

代码语言:javascript
复制
public final class StringBuilder extends AbstractStringBuilder implements java.io.Serializable, CharSequence{
  public StringBuilder append(String str) {
        super.append(str);
        return this;
  }

   public StringBuilder append(String str) {
        super.append(str);
        return this;
   }
}

StringBuffer

代码语言:javascript
复制
public final class StringBuffer extends AbstractStringBuilder
    implements java.io.Serializable, CharSequence{
  public StringBuffer(String str) {
        super(str.length() + 16);
        append(str);
  }
  public synchronized StringBuffer append(String str) {
        toStringCache = null;
        super.append(str);
        return this;
 }
}

StringBuffer和StringBuilder类似,最大的区别就是StringBuffer是线程安全的。

String、 StringBuffer 、StringBuilder的区别

1.都是final类,不允许被继承 2. String⻓度是不可变的,StringBuffer,StringBuilder⻓度是可变的 3. StringBuffer是线程安全的,StringBuilder不是线程安全的。但它们⽅法实现类似,StringBuffer在 ⽅法之上添加了synchronized修饰,保证线程安全 4. StringBuilder⽐StringBuffer拥有更好的性能 5. 如果⼀个String类型的字符串,在编译时可以确定是⼀个字符串常量,则编译完成之后,字符串会 ⾃动拼接成⼀个常量,此时String的速度⽐StringBuffer和StringBuilder的性能好的多

用例子解释下第五条:

代码语言:javascript
复制
public static void main(String[] args) {
        String a = "a";
        String b = "b";
        String c = a + b;
        String d = "a" + "b" + "c";
    }

看String d ,理解了吧?编译后直接为abc.

同时看string c的拼接过程,先⽣成⼀个StringBuilder对象,再调⽤2次append⽅法,最后再返回⼀个

String对象,知道String⽐StringBuilder慢的原因了吧。

那么也就是说,Java中的+对字符串的拼接,其实现原理是使用StringBuilder.append。

但是,String的使用+字符串拼接也不全都是基于StringBuilder.append,还有种特殊情况,那就是如果是两个固定的字面量拼接,如:

String d = "a" + "b" + "c";

编译器会进行常量折叠(因为两个都是编译期常量,编译期可知),直接变成 String d = "abc"。

StringUtils.join

除了JDK中内置的字符串拼接方法,还可以使用一些开源类库中提供的字符串拼接方法名,如apache.commons中提供的StringUtils类,其中的join方法可以拼接字符串。

代码语言:javascript
复制
 String a = "a";
 String b = "b";
 String result = StringUtils.join(a, ",", b);
 System.out.println(result);//a,b

这里简单说一下,StringUtils中提供的join方法,最主要的功能是:将数组或集合以某拼接符拼接到一起形成新的字符串,如:

代码语言:javascript
复制
 public static void main(String[] args) {
        List<String> list = Arrays.asList("编程识堂","每日更新Java相关技术文章,关注我不迷路");
        System.out.println( StringUtils.join(list,","));
     //编程识堂,每日更新Java相关技术文章,关注我不迷路
    }

原理

代码语言:javascript
复制
public static String join(Object[] array, char separator, int startIndex, int endIndex) {
        if (array == null) {
            return null;
        } else {
            int noOfItems = endIndex - startIndex;
            if (noOfItems <= 0) {
                return "";
            } else {
                StringBuilder buf = new StringBuilder(noOfItems * 16);

                for(int i = startIndex; i < endIndex; ++i) {
                    if (i > startIndex) {
                        buf.append(separator);
                    }

                    if (array[i] != null) {
                        buf.append(array[i]);
                    }
                }

                return buf.toString();
            }
        }
    }

通过查看StringUtils.join的源代码,我们可以发现,其实他也是通过StringBuilder来实现的。

StringJoiner

StringJoiner是java.util包中的一个类,用于构造一个由分隔符分隔的字符序列(可选),并且可以从提供的前缀开始并以提供的后缀结尾。虽然这也可以在StringBuilder类的帮助下在每个字符串之后附加分隔符,但StringJoiner提供了简单的方法来实现,而无需编写大量代码。

StringJoiner类共有2个构造函数,5个公有方法。其中最常用的方法就是add方法和toString方法,类似于StringBuilder中的append方法和toString方法。

演示

代码语言:javascript
复制
 public static void main(String[] args) {
        List<String> list = Arrays.asList("编程识堂","每日更新Java相关技术文章,关注我不迷路");
        StringJoiner stringJoiner = new StringJoiner(":","[","]");
        stringJoiner.add("编程识堂").add("每日更新Java相关技术文章,关注我不迷路");
        System.out.println( stringJoiner.toString());//[编程识堂:每日更新Java相关技术文章,关注我不迷路]

        String result = list.stream().collect(Collectors.joining("-", "{", "}"));
        System.out.println(result);//{编程识堂-每日更新Java相关技术文章,关注我不迷路}
    }

原理

主要看一下add方法:

代码语言:javascript
复制
 public StringJoiner(CharSequence delimiter,
                        CharSequence prefix,
                        CharSequence suffix) {
        Objects.requireNonNull(prefix, "The prefix must not be null");
        Objects.requireNonNull(delimiter, "The delimiter must not be null");
        Objects.requireNonNull(suffix, "The suffix must not be null");
        // make defensive copies of arguments
        this.prefix = prefix.toString();
        this.delimiter = delimiter.toString();
        this.suffix = suffix.toString();
        this.emptyValue = this.prefix + this.suffix;
    }

  public StringJoiner add(CharSequence newElement) {
        prepareBuilder().append(newElement);
        return this;
    }

  private StringBuilder prepareBuilder() {
        if (value != null) {
            value.append(delimiter);
        } else {
            value = new StringBuilder().append(prefix);
        }
        return value;
    }

看到了一个熟悉的身影——StringBuilder ,没错,StringJoiner其实就是依赖StringBuilder实现的。

当我们发现StringJoiner其实是通过StringBuilder实现之后,我们大概就可以猜到,他的性能损耗应该和直接使用StringBuilder差不多!

为什么要用StringJoiner

在了解了StringJoiner的用法和原理后,可能很多读者就会产生一个疑问,明明已经有一个StringBuilder了,为什么Java 8中还要定义一个StringJoiner呢?到底有什么好处呢?

如果读者足够了解Java 8的话,或许可以猜出个大概,这肯定和Stream有关。

代码语言:javascript
复制
 public static Collector<CharSequence, ?, String> joining(CharSequence delimiter,
                                                             CharSequence prefix,
                                                             CharSequence suffix) {
        return new CollectorImpl<>(
                () -> new StringJoiner(delimiter, prefix, suffix),
                StringJoiner::add, StringJoiner::merge,
                StringJoiner::toString, CH_NOID);
    }

我们通过Collectors.joining源码发现是通过StringJoiner实现的。

总结

如果日常开发中中,需要进行字符串拼接,如何选择?

1、如果只是简单的字符串拼接,考虑直接使用"+"即可。

2、如果是在for循环中进行字符串拼接,考虑使用StringBuilder和StringBuffer。

3、如果是通过一个List进行字符串拼接,则考虑使用StringJoiner。

StringJoiner其实是通过StringBuilder实现的,所以他的性能和StringBuilder差不多,他也是非线程安全的。

字符串有没有长度限制

想要搞清楚这个问题,首先我们需要翻阅一下String的源码,看下其中是否有关于长度的限制或者定义。

String类中有很多重载的构造函数,其中有几个是支持用户传入length来执行长度的:

代码语言:javascript
复制
 public String(byte bytes[], int offset, int length) {
        checkBounds(bytes, offset, length);
        this.value = StringCoding.decode(bytes, offset, length);
    }

可以看到,这里面的参数length是使用int类型定义的,那么也就是说,String定义的时候,最大支持的长度就是int的最大范围值。

根据Integer类的定义,java.lang.Integer#MAX_VALUE的最大值是2^31 - 1;

那么,我们是不是就可以认为String能支持的最大长度就是这个值了呢?

其实并不是,这个值只是在运行期,我们构造String的时候可以支持的一个最大长度,而实际上,在编译期,定义字符串的时候也是有长度限制的。

如以下代码:

String s = "11111...1111";//其中有10万个字符"1"

当我们使用如上形式定义一个字符串的时候,当我们执行javac编译时,是会抛出异常的,提示如下:

错误: 常量字符串过长

那么,明明String的构造函数指定的长度是可以支持2147483647(2^31 - 1)的,为什么像以上形式定义的时候无法编译呢?

其实,形如String s = "xxx";定义String的时候,xxx被我们称之为字面量,这种字面量在编译之后会以常量的形式进入到Class常量池。

那么问题就来了,因为要进入常量池,就要遵守常量池的有关规定。

所以字符串有长度限制,在编译期,要求字符串常量池中的常量不能超过65535,并且在javac执行过程中控制了最大值为65534。

在运行期,长度不能超过Int的范围,否则会抛异常。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2023-04-26,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 编程识堂 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 为什么说字符串是不可变的
    • 源码解析
      • 什么是不可变对象
        • 原因
        • 为什么String要设计成不可变
          • 缓存
            • 安全性
              • 线程安全
                • hashcode缓存
                  • 性能
                  • 字符串拼接
                    • 字符串不变性与字符串拼接
                      • 使用+拼接字符串
                        • 演示
                        • 原理
                      • concat
                        • 演示
                        • 原理
                      • StringBuffer和StringBuilder
                        • 演示
                        • 原理
                        • String、 StringBuffer 、StringBuilder的区别
                      • StringUtils.join
                        • 原理
                      • StringJoiner
                        • 演示
                        • 原理
                        • 为什么要用StringJoiner
                      • 总结
                      • 字符串有没有长度限制
                      领券
                      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档