Java >> Java tutorial >  >> Tag >> char

Vejledning til tegnkodning

1. Oversigt

I dette selvstudie vil vi diskutere det grundlæggende i tegnkodning, og hvordan vi håndterer det i Java.

2. Vigtigheden af ​​tegnkodning

Vi er ofte nødt til at beskæftige os med tekster, der tilhører flere sprog, med forskellige skriveskrifter som latin eller arabisk. Hvert tegn på hvert sprog skal på en eller anden måde kortlægges til et sæt etere og nuller. Det er virkelig et under, at computere kan behandle alle vores sprog korrekt.

For at gøre dette korrekt skal vi tænke på tegnkodning. Hvis du ikke gør det, kan det ofte føre til tab af data og endda sikkerhedssårbarheder.

For at forstå dette bedre, lad os definere en metode til at afkode en tekst i Java:

String decodeText(String input, String encoding) throws IOException {
    return 
      new BufferedReader(
        new InputStreamReader(
          new ByteArrayInputStream(input.getBytes()), 
          Charset.forName(encoding)))
        .readLine();
}

Bemærk, at den inputtekst, vi feeder her, bruger standardplatformens kodning.

Hvis vi kører denne metode med input som "Facademønsteret er et softwaredesignmønster." og kodning som "US-ASCII" , udsender det:

The fa��ade pattern is a software design pattern.

Nå, ikke lige hvad vi forventede.

Hvad kunne være gået galt? Vi vil forsøge at forstå og rette dette i resten af ​​dette selvstudie.

3. Grundlæggende

Før vi graver dybere, lad os dog hurtigt gennemgå tre udtryk:kodningtegnsæt , og kodepunkt .

3.1. Kodning

Computere kan kun forstå binære repræsentationer som 1 og 0 . At behandle alt andet kræver en form for kortlægning fra teksten i den virkelige verden til dens binære repræsentation. Denne kortlægning er, hvad vi kender som tegnkodning eller blot som kodning .

For eksempel, det første bogstav i vores besked, "T", i US-ASCII koder  til "01010100".

3.2. Tegnsæt

Tilknytningen af ​​tegn til deres binære repræsentationer kan variere meget med hensyn til de tegn, de inkluderer. Antallet af tegn, der indgår i en kortlægning, kan variere fra kun få til alle de tegn, der er i praktisk brug. Det sæt af tegn, der er inkluderet i en tilknytningsdefinition, kaldes formelt et tegnsæt .

For eksempel har ASCII et tegnsæt på 128 tegn.

3.3. Kodepunkt

Et kodepunkt er en abstraktion, der adskiller et tegn fra dets faktiske kodning. Et kodepunkt er en heltalsreference til et bestemt tegn.

Vi kan repræsentere selve hele tallet i almindelig decimal eller alternative baser som hexadecimal eller oktal. Vi bruger alternative baser for at lette henvisningen af ​​store tal.

For eksempel har det første bogstav i vores besked, T, i Unicode et kodepunkt "U+0054" (eller 84 i decimal).

4. Forstå kodningsskemaer

En tegnkodning kan have forskellige former afhængigt af antallet af tegn, den koder.

Antallet af kodede tegn har en direkte relation til længden af ​​hver repræsentation, som typisk måles som antallet af bytes. At have flere tegn at indkode betyder i bund og grund at have brug for længere binære repræsentationer.

Lad os gennemgå nogle af de populære kodningsskemaer i praksis i dag.

4.1. Single-byte-kodning

Et af de tidligste kodningsskemaer, kaldet ASCII (American Standard Code for Information Exchange) bruger et enkelt byte-kodningsskema. Dette betyder i bund og grund, at hvert tegn i ASCII er repræsenteret med syv-bit binære tal. Dette efterlader stadig en bit fri i hver byte!

ASCII's sæt på 128 tegn dækker engelske alfabeter med små og store bogstaver, cifre og nogle special- og kontroltegn.

Lad os definere en simpel metode i Java til at vise den binære repræsentation for et tegn under et bestemt kodningsskema:

String convertToBinary(String input, String encoding) 
      throws UnsupportedEncodingException {
    byte[] encoded_input = Charset.forName(encoding)
      .encode(input)
      .array();  
    return IntStream.range(0, encoded_input.length)
        .map(i -> encoded_input[i])
        .mapToObj(e -> Integer.toBinaryString(e ^ 255))
        .map(e -> String.format("%1$" + Byte.SIZE + "s", e).replace(" ", "0"))
        .collect(Collectors.joining(" "));
}

Nu har tegnet 'T' et kodepunkt på 84 i US-ASCII (ASCII omtales som US-ASCII i Java).

Og hvis vi bruger vores hjælpemetode, kan vi se dens binære repræsentation:

assertEquals(convertToBinary("T", "US-ASCII"), "01010100");

Dette er, som vi forventede, en syv-bit binær repræsentation for tegnet 'T'.

Den originale ASCII efterlod den mest betydningsfulde bit af hver byte ubrugt. Samtidig havde ASCII efterladt en del tegn urepræsenteret, især for ikke-engelske sprog.

Dette førte til et forsøg på at bruge den ubrugte bit og inkludere yderligere 128 tegn.

Der var adskillige variationer af ASCII-kodningsskemaet foreslået og vedtaget over tiden. Disse blev løst omtalt som "ASCII-udvidelser".

Mange af ASCII-udvidelserne havde forskellige succesniveauer, men dette var naturligvis ikke godt nok til en bredere adoption, da mange karakterer stadig ikke var repræsenteret.

En af de mere populære ASCII-udvidelser var ISO-8859-1 , også omtalt som "ISO Latin 1".

4.2. Multi-Byte-kodning

Efterhånden som behovet for at rumme flere og flere tegn voksede, var enkeltbyte-kodningsskemaer som ASCII ikke holdbare.

Dette gav anledning til multi-byte-kodningssystemer, som har en meget bedre kapacitet, omend på bekostning af øget pladsbehov.

BIG5 og SHIFT-JIS er eksempler på multi-byte tegnkodningsskemaer, der begyndte at bruge én og to bytes til at repræsentere bredere tegnsæt . De fleste af disse blev oprettet for behovet for at repræsentere kinesiske og lignende scripts, som har et betydeligt højere antal tegn.

Lad os nu kalde metoden convertToBinary med input som '語', et kinesisk tegn og kodning som "Big5":

assertEquals(convertToBinary("語", "Big5"), "10111011 01111001");

Outputtet ovenfor viser, at Big5-kodning bruger to bytes til at repræsentere tegnet '語'.

En omfattende liste over tegnkodninger, sammen med deres aliaser, vedligeholdes af International Number Authority.

5. Unicode

Det er ikke svært at forstå, at selvom kodning er vigtig, er afkodning lige så vigtig for at give mening med repræsentationerne. Dette er kun muligt i praksis, hvis et konsekvent eller kompatibelt kodningsskema bruges i vid udstrækning.

Forskellige indkodningsskemaer udviklet isoleret og praktiseret i lokale geografier begyndte at blive udfordrende.

Denne udfordring gav anledning til en enestående kodningsstandard kaldet Unicode, som har kapacitet til alle mulige tegn i verden . Dette inkluderer de tegn, der er i brug, og endda dem, der er nedlagte!

Nå, det skal kræve flere bytes for at gemme hvert tegn? Helt ærligt ja, men Unicode har en genial løsning.

Unicode som standard definerer kodepunkter for alle mulige tegn i verden. Kodepunktet for tegnet 'T' i Unicode er 84 i decimal. Vi refererer generelt til dette som "U+0054" i Unicode, som ikke er andet end U+ efterfulgt af det hexadecimale tal.

Vi bruger hexadecimal som basis for kodepunkter i Unicode, da der er 1.114.112 punkter, hvilket er et ret stort tal at kommunikere bekvemt i decimaler!

Hvordan disse kodepunkter kodes til bits er overladt til specifikke kodningsskemaer i Unicode. Vi vil dække nogle af disse kodningsskemaer i underafsnittene nedenfor.

5.1. UTF-32

UTF-32 er et kodningsskema til Unicode, der anvender fire bytes til at repræsentere hvert kodepunkt defineret af Unicode. Det er klart, at det er pladsineffektivt at bruge fire bytes for hvert tegn.

Lad os se, hvordan et simpelt tegn som 'T' er repræsenteret i UTF-32. Vi vil bruge metoden convertToBinary indført tidligere:

assertEquals(convertToBinary("T", "UTF-32"), "00000000 00000000 00000000 01010100");

Outputtet ovenfor viser brugen af ​​fire bytes til at repræsentere tegnet "T", hvor de første tre bytes bare er spildplads.

5.2. UTF-8

UTF-8 er et andet kodningsskema for Unicode, som anvender en variabel længde af bytes til at kode . Selvom den generelt bruger en enkelt byte til at kode tegn, kan den bruge et højere antal bytes, hvis det er nødvendigt, og dermed spare plads.

Lad os igen kalde metoden convertToBinary med input som 'T' og kodning som "UTF-8":

assertEquals(convertToBinary("T", "UTF-8"), "01010100");

Outputtet ligner nøjagtigt ASCII ved brug af kun en enkelt byte. Faktisk er UTF-8 fuldstændig bagudkompatibel med ASCII.

Lad os igen kalde metoden convertToBinary med input som '語' og kodning som "UTF-8":

assertEquals(convertToBinary("語", "UTF-8"), "11101000 10101010 10011110");

Som vi kan se her, bruger UTF-8 tre bytes til at repræsentere tegnet '語'. Dette er kendt som kodning med variabel bredde .

UTF-8 er på grund af sin pladseffektivitet den mest almindelige kodning, der bruges på nettet.

6. Understøttelse af kodning i Java

Java understøtter en bred vifte af kodninger og deres konverteringer til hinanden. Klassen Charset definerer et sæt standardkodninger, som enhver implementering af Java-platformen er forpligtet til at understøtte.

Dette inkluderer US-ASCII, ISO-8859-1, UTF-8 og UTF-16 for at nævne nogle få. En bestemt implementering af Java kan eventuelt understøtte yderligere kodninger .

Der er nogle finesser i den måde, Java opfanger et tegnsæt at arbejde med. Lad os gennemgå dem mere detaljeret.

6.1. Standard tegnsæt

Java-platformen afhænger i høj grad af en egenskab kaldet standardtegnsættet . Java Virtual Machine (JVM) bestemmer standardtegnsættet under opstart .

Dette afhænger af lokaliteten og tegnsættet for det underliggende operativsystem, som JVM kører på. For eksempel på MacOS er standardtegnsættet UTF-8.

Lad os se, hvordan vi kan bestemme standardtegnsættet:

Charset.defaultCharset().displayName();

Hvis vi kører dette kodestykke på en Windows-maskine, får vi output:

windows-1252

Nu er "windows-1252" standardtegnsættet for Windows-platformen på engelsk, som i dette tilfælde har bestemt standardtegnsættet for JVM, som kører på Windows.

6.2. Hvem bruger standardtegnsættet?

Mange af Java API'erne gør brug af standardtegnsættet som bestemt af JVM. For at nævne nogle få:

  • InputStreamReader og FileReader
  • OutputStreamWriter og FileWriter
  • Formater og Scanner
  • URLEncoder og URLDecoder

Så det betyder, at hvis vi kører vores eksempel uden at angive tegnsættet:

new BufferedReader(new InputStreamReader(new ByteArrayInputStream(input.getBytes()))).readLine();

så ville den bruge standardtegnsættet til at afkode det.

Og der er flere API'er, der gør det samme valg som standard.

Standardtegnsættet antager derfor en vigtighed, som vi ikke sikkert kan ignorere.

6.3. Problemer med standardtegnsættet

Som vi har set, bestemmes standardtegnsættet i Java dynamisk, når JVM starter. Dette gør platformen mindre pålidelig eller fejltilbøjelig, når den bruges på tværs af forskellige operativsystemer.

For eksempel, hvis vi kører

new BufferedReader(new InputStreamReader(new ByteArrayInputStream(input.getBytes()))).readLine();

på macOS vil den bruge UTF-8.

Hvis vi prøver det samme uddrag på Windows, vil det bruge Windows-1252 til at afkode den samme tekst.

Eller forestil dig at skrive en fil på en macOS og derefter læse den samme fil på Windows.

Det er ikke svært at forstå, at dette på grund af forskellige kodningsskemaer kan føre til datatab eller korruption.

6.4. Kan vi tilsidesætte standardtegnsættet?

Bestemmelsen af ​​standardtegnsættet i Java fører til to systemegenskaber:

  • fil.kodning :Værdien af ​​denne systemegenskab er navnet på standardtegnsættet
  • sun.jnu.encoding :Værdien af ​​denne systemegenskab er navnet på det tegnsæt, der bruges ved kodning/afkodning af filstier

Nu er det intuitivt at tilsidesætte disse systemegenskaber gennem kommandolinjeargumenter:

-Dfile.encoding="UTF-8"
-Dsun.jnu.encoding="UTF-8"

Det er dog vigtigt at bemærke, at disse egenskaber er skrivebeskyttet i Java. Deres brug som ovenfor er ikke til stede i dokumentationen . Tilsidesættelse af disse systemegenskaber har muligvis ikke ønsket eller forudsigelig adfærd.

Derfor bør vi undgå at tilsidesætte standardtegnsættet i Java .

6.5. Hvorfor løser Java ikke dette?

Der er et Java Enhancement Proposal (JEP), som foreskriver brug af "UTF-8" som standardtegnsæt i Java i stedet for at basere det på lokalitet og operativsystemets tegnsæt.

Denne JEP er i en udkasttilstand lige nu, og når den (forhåbentlig!) går igennem, vil den løse de fleste af de problemer, vi diskuterede tidligere.

Bemærk, at de nyere API'er kan lide dem i java.nio.file.Files brug ikke standardtegnsættet. Metoderne i disse API'er læser eller skriver tegnstrømme med tegnsæt som UTF-8 i stedet for standardtegnsæt.

6.6. Løsning af dette problem i vores programmer

Vi bør normalt vælge at angive et tegnsæt, når vi håndterer tekst i stedet for at stole på standardindstillingerne . Vi kan udtrykkeligt erklære den kodning, vi ønsker at bruge i klasser, der omhandler tegn-til-byte-konverteringer.

Heldigvis specificerer vores eksempel allerede tegnsættet. Vi skal bare vælge den rigtige og lade Java klare resten.

Vi burde nu indse, at accenttegn som "ç" ikke er til stede i kodningsskemaet ASCII, og vi har derfor brug for en kodning, der inkluderer dem. Måske UTF-8?

Lad os prøve det, vi vil nu køre metoden decodeText  med samme input men kodning som "UTF-8":

The façade pattern is a software-design pattern.

Bingo! Vi kan se det output, vi håbede at se nu.

Her har vi indstillet den kodning, vi mener passer bedst til vores behov i konstruktøren af ​​InputStreamReader . Dette er normalt den sikreste metode til at håndtere tegn og bytekonverteringer i Java.

Tilsvarende OutputStreamWriter og mange andre API'er understøtter indstilling af et kodningsskema gennem deres konstruktør.

6.7. MalformedInputException

Når vi afkoder en bytesekvens, findes der tilfælde, hvor det ikke er lovligt for det givne tegnsæt , eller også er det ikke en lovlig seksten-bit Unicode. Med andre ord har den givne byte-sekvens ingen mapping i det angivne Charset .

Der er tre foruddefinerede strategier (eller CodingErrorAction ) når inputsekvensen har forkert udformet input:

  • IGNORER ignorerer forkerte tegn og genoptager kodningen
  • UDSKIFT erstatter de forkerte tegn i outputbufferen og genoptager kodningsoperationen
  • RAPPORT vil kaste en MalformedInputException

Standard malformedInputAction for CharsetDecoder er REPORT, og standarden malformedInputAction af standarddekoderen i InputStreamReader er REPLACE.

Lad os definere en afkodningsfunktion, der modtager et specificeret tegnsæt , en CodingErrorAction type og en streng, der skal afkodes:

String decodeText(String input, Charset charset, 
  CodingErrorAction codingErrorAction) throws IOException {
    CharsetDecoder charsetDecoder = charset.newDecoder();
    charsetDecoder.onMalformedInput(codingErrorAction);
    return new BufferedReader(
      new InputStreamReader(
        new ByteArrayInputStream(input.getBytes()), charsetDecoder)).readLine();
}

Så hvis vi afkoder "Facademønsteret er et softwaredesignmønster." med US_ASCII , ville outputtet for hver strategi være forskelligt. Først bruger vi CodingErrorAction.IGNORE som springer ulovlige tegn over:

Assertions.assertEquals(
  "The faade pattern is a software design pattern.",
  CharacterEncodingExamples.decodeText(
    "The façade pattern is a software design pattern.",
    StandardCharsets.US_ASCII,
    CodingErrorAction.IGNORE));

Til den anden test bruger vi CodingErrorAction.REPLACE der sætter � i stedet for de ulovlige tegn:

Assertions.assertEquals(
  "The fa��ade pattern is a software design pattern.",
  CharacterEncodingExamples.decodeText(
    "The façade pattern is a software design pattern.",
    StandardCharsets.US_ASCII,
    CodingErrorAction.REPLACE));

Til den tredje test bruger vi CodingErrorAction.REPORT hvilket fører til at kaste MalformedInputException:

Assertions.assertThrows(
  MalformedInputException.class,
    () -> CharacterEncodingExamples.decodeText(
      "The façade pattern is a software design pattern.",
      StandardCharsets.US_ASCII,
      CodingErrorAction.REPORT));

7. Andre steder, hvor kodning er vigtig

Vi behøver ikke kun at overveje tegnkodning under programmering. Tekster kan gå endegyldigt galt mange andre steder.

Den mest almindelige årsag til problemer i disse tilfælde er konvertering af tekst fra et kodningsskema til et andet , og derved muligvis indføre datatab.

Lad os hurtigt gennemgå et par steder, hvor vi kan støde på problemer ved indkodning eller afkodning af tekst.

7.1. Teksteditorer

I de fleste tilfælde er en teksteditor det sted, hvor teksterne stammer fra. Der er adskillige teksteditorer i populært valg, herunder vi, Notesblok og MS Word. De fleste af disse teksteditorer giver os mulighed for at vælge kodningsskemaet. Derfor bør vi altid sikre os, at de passer til den tekst, vi håndterer.

7.2. Filsystem

Efter at vi har oprettet tekster i en editor, skal vi gemme dem i et eller andet filsystem. Filsystemet afhænger af det operativsystem, det kører på. De fleste operativsystemer har iboende understøttelse af flere kodningsskemaer. Der kan dog stadig være tilfælde, hvor en kodningskonvertering fører til datatab.

7.3. Netværk

Tekster, når de overføres over et netværk ved hjælp af en protokol som File Transfer Protocol (FTP), involverer også konvertering mellem tegnkodninger. For alt, der er kodet i Unicode, er det sikrest at overføre som binært for at minimere risikoen for tab ved konvertering. Overførsel af tekst over et netværk er dog en af ​​de mindre hyppige årsager til datakorruption.

7.4. Databaser

De fleste af de populære databaser som Oracle og MySQL understøtter valget af tegnkodningsskemaet ved installation eller oprettelse af databaser. Vi skal vælge dette i overensstemmelse med de tekster, vi forventer at gemme i databasen. Dette er et af de hyppigere steder, hvor korruption af tekstdata sker på grund af kodningskonverteringer.

7.5. Browsere

Endelig, i de fleste webapplikationer, opretter vi tekster og sender dem gennem forskellige lag med den hensigt at se dem i en brugergrænseflade, som en browser. Også her er det bydende nødvendigt for os at vælge den rigtige tegnkodning, som kan vise tegnene korrekt. De fleste populære browsere som Chrome, Edge tillader valg af tegnkodning gennem deres indstillinger.

8. Konklusion

I denne artikel diskuterede vi, hvordan kodning kan være et problem under programmering.

Vi diskuterede yderligere det grundlæggende, herunder kodning og tegnsæt. Desuden gennemgik vi forskellige kodningsskemaer og deres anvendelser.

Vi fandt også et eksempel på forkert tegnkodningsbrug i Java og så, hvordan man får det rigtigt. Til sidst diskuterede vi nogle andre almindelige fejlscenarier relateret til tegnkodning.

Som altid er koden til eksemplerne tilgængelig på GitHub.


Java tag