Veiledning til tegnkoding

1. Oversikt

I denne opplæringen vil vi diskutere det grunnleggende om tegnkoding og hvordan vi håndterer det i Java.

2. Viktigheten av tegnkoding

Vi må ofte håndtere tekster som tilhører flere språk med forskjellige skriftskrifter som latin eller arabisk. Hvert tegn på hvert språk må på en eller annen måte kartlegges til et sett med ener og nuller. Egentlig er det et rart at datamaskiner kan behandle alle språkene våre riktig.

For å gjøre dette ordentlig, vi må tenke på koding av tegn. Å ikke gjøre det kan ofte føre til tap av data og til og med sikkerhetsproblemer.

For å forstå dette bedre, la oss definere en metode for å dekode en tekst i Java:

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

Merk at inndatateksten vi mater her bruker standard plattformkoding.

Hvis vi kjører denne metoden med inngang som "Fasademønsteret er et mønster for programvaredesign." og koding som “US-ASCII”, det vil sende ut:

Fasademønsteret er et programvaremønster.

Vel, ikke akkurat det vi forventet.

Hva kunne ha gått galt? Vi vil prøve å forstå og rette dette i resten av denne opplæringen.

3. Grunnleggende

Før vi graver dypere, la oss raskt gjennomgå tre termer: koding, charsets, og kodepunkt.

3.1. Koding

Datamaskiner kan bare forstå binære representasjoner som 1 og 0. Behandling av noe annet krever en slags kartlegging fra den virkelige teksten til den binære representasjonen. Denne kartleggingen er det vi kjenner til tegnkoding eller bare som koding.

For eksempel den første bokstaven i meldingen vår, “T”, i US-ASCII koder til “01010100”.

3.2. Charsets

Kartleggingen av tegn til deres binære representasjoner kan variere sterkt når det gjelder tegnene de inkluderer. Antall tegn som er inkludert i en kartlegging kan variere fra bare noen få til alle tegnene i praktisk bruk. Settet med tegn som er inkludert i en kartleggingsdefinisjon kalles formelt a charset.

For eksempel har ASCII et tegnsett på 128 tegn.

3.3. Kodepunkt

Et kodepunkt er en abstraksjon som skiller et tegn fra dets faktiske koding. EN kodepunkt er et heltall referanse til et bestemt tegn.

Vi kan representere selve heltallet i ren desimal eller alternative baser som heksadesimal eller oktal. Vi bruker alternative baser for å gjøre det enkelt å henvise store tall.

For eksempel har den første bokstaven i meldingen vår, T, i Unicode et kodepunkt "U + 0054" (eller 84 i desimal).

4. Forstå kodingsplaner

En tegnkoding kan ha forskjellige former avhengig av antall tegn den koder.

Antall kodede tegn har et direkte forhold til lengden på hver representasjon som vanligvis måles som antall byte. Å ha flere tegn å kode betyr egentlig at du trenger lengre binære representasjoner.

La oss gå gjennom noen av de populære kodingsordningene i praksis i dag.

4.1. Enkeltbyte-koding

En av de tidligste kodingsordningene, kalt ASCII (American Standard Code for Information Exchange), bruker en enkelt byte-kodingsplan. Dette betyr egentlig det hvert tegn i ASCII er representert med syv-bits binære tall. Dette etterlater fortsatt en bit gratis i hver byte!

ASCIIs 128-tegnsett dekker engelske alfabet i små og store bokstaver, sifre og noen spesial- og kontrolltegn.

La oss definere en enkel metode i Java for å vise den binære representasjonen for et tegn under et bestemt kodeskjema:

String convertToBinary (strenginngang, strengkoding) kaster UnsupportedEncodingException {byte [] encoded_input = Charset.forName (koding). 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 (" ")); }

Nå har tegnet ‘T’ et kodepunkt på 84 i US-ASCII (ASCII blir referert til som US-ASCII i Java).

Og hvis vi bruker verktøymetoden vår, kan vi se dens binære representasjon:

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

Dette, som vi forventet, er en syv-bit binær representasjon for tegnet ‘T’.

Den opprinnelige ASCII etterlot den viktigste biten av hver byte ubrukt. Samtidig hadde ASCII latt ganske mange tegn være ikke-representert, spesielt for ikke-engelskspråklige språk.

Dette førte til et forsøk på å bruke den ubrukt biten og inkluderer ytterligere 128 tegn.

Det var flere varianter av ASCII-kodingsordningen som ble foreslått og vedtatt over tid. Disse ble løst referert til som “ASCII-utvidelser”.

Mange av ASCII-utvidelsene hadde forskjellige suksessnivåer, men åpenbart var dette ikke bra nok for bredere adopsjon, ettersom mange tegn fortsatt ikke var representert.

En av de mer populære ASCII-utvidelsene var ISO-8859-1, også referert til som “ISO Latin 1”.

4.2. Multi-byte-koding

Etter hvert som behovet for å imøtekomme flere og flere tegn vokste, var enkeltbyte-kodingsordninger som ASCII ikke bærekraftige.

Dette ga opphav til flerbyte-kodingsordninger som har mye bedre kapasitet, om enn på bekostning av økt plassbehov.

BIG5 og SHIFT-JIS er eksempler på multi-byte koding ordninger som begynte å bruke en i tillegg til to byte for å representere bredere tegnsett. De fleste av disse ble opprettet for behovet for å representere kinesiske og lignende skript som har et betydelig høyere antall tegn.

La oss nå kalle metoden convertToBinary med inngang som ‘語’, en kinesisk karakter, og koding som “Big5”:

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

Utgangen ovenfor viser at Big5-koding bruker to byte til å representere tegnet ‘語’.

En omfattende liste over tegnkodinger, sammen med aliasene, vedlikeholdes av International Number Authority.

5. Unicode

Det er ikke vanskelig å forstå at selv om koding er viktig, er dekoding like viktig for å gi mening om representasjonene. Dette er bare mulig i praksis hvis en konsistent eller kompatibel kodingsplan brukes mye.

Ulike kodingsordninger utviklet isolert og praktisert i lokale geografier begynte å bli utfordrende.

Denne utfordringen ga opphav til en enestående kodingsstandard kalt Unicode som har kapasitet til alle mulige karakterer i verden. Dette inkluderer tegnene som er i bruk, og til og med de som er nedlagte!

Vel, det må kreve flere byte for å lagre hvert tegn? Ærlig talt ja, men Unicode har en genial løsning.

Unicode definerer som standard kodepunkter for alle mulige tegn i verden. Kodepunktet for tegnet ‘T’ i Unicode er 84 i desimal. Vi refererer generelt til dette som "U + 0054" i Unicode, som ikke er annet enn U + etterfulgt av heksadesimaltallet.

Vi bruker heksadesimal som base for kodepunkter i Unicode, da det er 1114,112 poeng, som er et ganske stort tall for å kommunisere praktisk i desimal!

Hvordan disse kodepunktene blir kodet i biter, overlates til spesifikke kodingsoppsett i Unicode. Vi vil dekke noen av disse kodingsskjemaene i underavsnittene nedenfor.

5.1. UTF-32

UTF-32 er et kodeskjema for Unicode som benytter fire byte til å representere hvert kodepunkt definert av Unicode. Åpenbart er det plasseffektivt å bruke fire byte for hvert tegn.

La oss se hvordan en enkel karakter som ‘T’ er representert i UTF-32. Vi vil bruke metoden convertToBinary introdusert tidligere:

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

Utgangen ovenfor viser bruken av fire byte for å representere tegnet ‘T’ der de tre første bytene bare er bortkastet plass.

5.2. UTF-8

UTF-8 er et annet kodeskjema for Unicode som bruker en variabel lengde på byte som skal kodes. Selv om den bruker en enkelt byte til å kode tegn generelt, kan den bruke et større antall byte om nødvendig, og dermed spare plass.

La oss igjen kalle metoden convertToBinary med inngang som ‘T’ og koding som “UTF-8”:

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

Utgangen er nøyaktig lik ASCII med bare en enkelt byte. UTF-8 er faktisk helt bakoverkompatibel med ASCII.

La oss igjen kalle metoden convertToBinary med inngang som ‘語’ og koding som “UTF-8”:

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

Som vi kan se her bruker UTF-8 tre byte til å representere tegnet ‘語’. Dette er kjent som koding med variabel bredde.

UTF-8 er på grunn av sin plasseffektivitet den vanligste kodingen som brukes på nettet.

6. Kodingstøtte i Java

Java støtter et bredt utvalg av kodinger og deres konverteringer til hverandre. Klassen Charset definerer et sett med standard kodinger som hver implementering av Java-plattform har mandat til å støtte.

Dette inkluderer US-ASCII, ISO-8859-1, UTF-8 og UTF-16 for å nevne noen. En bestemt implementering av Java kan eventuelt støtte ytterligere kodinger.

Det er noen finesser i måten Java plukker opp et tegnsett å jobbe med. La oss gå gjennom dem i flere detaljer.

6.1. Standard tegnsett

Java-plattformen er avhengig av en eiendom som heter standardtegnsettet. Java Virtual Machine (JVM) bestemmer standard tegnsett under oppstart.

Dette er avhengig av lokaliteten og tegnsettet til det underliggende operativsystemet som JVM kjører på. For eksempel på MacOS er standard tegnsett UTF-8.

La oss se hvordan vi kan bestemme standardtegnsettet:

Charset.defaultCharset (). DisplayName ();

Hvis vi kjører denne kodebiten på en Windows-maskin, får vi utdataene:

windows-1252

Nå er "windows-1252" standardtegnsettet til Windows-plattformen på engelsk, som i dette tilfellet har bestemt standardtegnsettet til JVM som kjører på Windows.

6.2. Hvem bruker standardtegnsettet?

Mange av Java API-ene bruker standard tegnsett som bestemt av JVM. For å nevne noen:

  • InputStreamReader og FileReader
  • OutputStreamWriter og FileWriter
  • Formaterer og Skanner
  • URLEncoder og URLDecoder

Så dette betyr at hvis vi kjører eksemplet vårt uten å spesifisere tegnsettet:

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

da vil den bruke standardtegnsettet til å dekode det.

Og det er flere API-er som gjør det samme valget som standard.

Standardtegnsettet antar derfor en betydning som vi ikke trygt kan ignorere.

6.3. Problemer med standardtegnsettet

Som vi har sett at standard tegnsett i Java bestemmes dynamisk når JVM starter. Dette gjør plattformen mindre pålitelig eller feilutsatt når den brukes på tvers av forskjellige operativsystemer.

For eksempel hvis vi løper

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

på macOS vil den bruke UTF-8.

Hvis vi prøver den samme kodebiten på Windows, vil den bruke Windows-1252 til å dekode den samme teksten.

Eller forestill deg å skrive en fil på en macOS, og deretter lese den samme filen på Windows.

Det er ikke vanskelig å forstå at dette på grunn av forskjellige kodingsordninger kan føre til tap av data eller korrupsjon.

6.4. Kan vi overstyre standardtegnsettet?

Bestemmelsen av standardtegnsettet i Java fører til to systemegenskaper:

  • file.encoding: Verdien til denne systemegenskapen er navnet på standardtegnsettet
  • sun.jnu.encoding: Verdien til denne systemegenskapen er navnet på tegnsettet som brukes ved koding / dekoding av filbaner

Nå er det intuitivt å overstyre disse systemegenskapene gjennom kommandolinjeargumenter:

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

Det er imidlertid viktig å merke seg at disse egenskapene er skrivebeskyttet i Java. Deres bruk som ovenfor er ikke til stede i dokumentasjonen. Å overstyre disse systemegenskapene har kanskje ikke ønsket eller forutsigbar oppførsel.

Derfor, vi bør unngå å overstyre standardtegnsettet i Java.

6.5. Hvorfor løser ikke Java dette?

Det er et Java Enhancement Proposal (JEP) som foreskriver bruk av "UTF-8" som standard tegnsett i Java i stedet for å basere det på lokale og operativsystem-sett.

Denne JEP er i utkaststilstand per nå, og når den (forhåpentligvis!) Går gjennom, vil den løse de fleste spørsmålene vi diskuterte tidligere.

Merk at de nyere API-ene som de i java.nio.file.Files ikke bruk standardtegnsettet. Metodene i disse API-ene leser eller skriver tegnstrømmer med tegnsett som UTF-8 i stedet for standard tegnsett.

6.6. Løse dette problemet i programmene våre

Vi burde normalt velg å spesifisere et tegnsett når du arbeider med tekst i stedet for å stole på standardinnstillingene. Vi kan eksplisitt erklære kodingen vi vil bruke i klasser som håndterer karakter-til-byte-konverteringer.

Heldigvis er eksemplet vårt allerede å spesifisere tegnsettet. Vi trenger bare å velge den rette og la Java gjøre resten.

Vi skal nå innse at tegn med aksent som 'ç' ikke er tilstede i kodingsskjemaet ASCII, og derfor trenger vi en koding som inkluderer dem. Kanskje UTF-8?

La oss prøve det, vi vil nå kjøre metoden dekode Tekst med samme inngang, men koding som “UTF-8”:

Fasademønsteret er et mønster for programvaredesign.

Bingo! Vi kan se resultatet vi håpet å se nå.

Her har vi satt kodingen vi synes passer best til vårt behov i konstruktøren av InputStreamReader. Dette er vanligvis den sikreste metoden for å håndtere tegn og bytekonvertering i Java.

På samme måte, OutputStreamWriter og mange andre API-er støtter å sette et kodeskjema gjennom konstruktøren.

6.7. Feilformet InputException

Når vi dekoder en bytesekvens, finnes det tilfeller der det ikke er lovlig for det gitte Charset, ellers er det ikke en lovlig seksten-biters Unicode. Med andre ord har den gitte bytesekvensen ingen kartlegging i det angitte Charset.

Det er tre forhåndsdefinerte strategier (eller CodingErrorAction) når inngangssekvensen har feil format:

  • OVERSE vil ignorere misdannede tegn og fortsette kodingen
  • ERSTATTE erstatter de feilformede tegnene i utgangsbufferen og fortsetter kodingsoperasjonen
  • RAPPORTERE vil kaste en Feilformet InputException

Standaren feilformetInputAction for CharsetDecoder er RAPPORT, og standard feilformetInputAction av standard dekoderen i InputStreamReader er ERSTATTE.

La oss definere en avkodingsfunksjon som mottar en spesifisert Charset, a CodingErrorAction type, og en streng som skal dekodes:

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

Så hvis vi dekoder "Fasademønsteret er et programvaredesignmønster." med US_ASCIIville produksjonen for hver strategi være annerledes. Først bruker vi CodingErrorAction.IGNORE som hopper over ulovlige tegn:

Assertions.assertEquals ("Fademønsteret er et programvaremønster.", CharacterEncodingExamples.decodeText ("Fasademønsteret er et mønster for programvaredesign.", StandardCharsets.US_ASCII, CodingErrorAction.IGNORE));

For den andre testen bruker vi CodingErrorAction.REPLACE som setter i stedet for de ulovlige karakterene:

Assertions.assertEquals ("Fasademønsteret er et mønster for programvaredesign.", CharacterEncodingExamples.decodeText ("Fasademønsteret er et mønster for programvaredesign.", StandardCharsets.US_ASCII, CodingErrorAction.REPLACE));

For den tredje testen bruker vi CodingErrorAction.REPORT som fører til kasting Feilformet InputException:

Assertions.assertThrows (MalformedInputException.class, () -> CharacterEncodingExamples.decodeText ("Fasademønsteret er et programvaredesignmønster.", StandardCharsets.US_ASCII, CodingErrorAction.REPORT));

7. Andre steder der koding er viktig

Vi trenger ikke bare vurdere karakterkoding mens vi programmerer. Tekster kan gå galt terminalt mange andre steder.

De den vanligste årsaken til problemer i disse tilfellene er konvertering av tekst fra ett kodeskjema til et annet, derved muligens innføring av tap av data.

La oss raskt gå gjennom noen få steder der vi kan støte på problemer når vi koder eller dekoder tekst.

7.1. Tekstredigerere

I de fleste tilfeller er en tekstredigerer der teksten har sitt utspring. Det er mange tekstredigerere som er populære, inkludert vi, Notisblokk og MS Word. De fleste av disse tekstredigererne tillater oss å velge kodeskjema. Derfor bør vi alltid sørge for at de passer til teksten vi håndterer.

7.2. Filsystem

Når vi har laget tekster i en redaktør, må vi lagre dem i et filsystem. Filsystemet avhenger av operativsystemet det kjører på. De fleste operativsystemer har iboende støtte for flere kodingsordninger. Imidlertid kan det fortsatt være tilfeller der en kodingskonvertering fører til tap av data.

7.3. Nettverk

Tekster når de overføres over et nettverk ved hjelp av en protokoll som File Transfer Protocol (FTP), involverer også konvertering mellom tegnkodinger. For alt som er kodet i Unicode, er det tryggest å overføre som binært for å minimere risikoen for tap i konvertering. Overføring av tekst over et nettverk er imidlertid en av de sjeldnere årsakene til datakorrupsjon.

7.4. Databaser

De fleste av de populære databasene som Oracle og MySQL støtter valget av tegnkodingsskjema ved installasjon eller oppretting av databaser. Vi må velge dette i samsvar med tekstene vi forventer å lagre i databasen. Dette er et av de hyppigere stedene der korrupsjon av tekstdata skjer på grunn av koding av konverteringer.

7.5. Nettlesere

Til slutt, i de fleste webapplikasjoner, lager vi tekster og sender dem gjennom forskjellige lag med den hensikt å se dem i et brukergrensesnitt, som en nettleser. Også her er det avgjørende for oss å velge riktig tegnkoding som kan vise tegnene riktig. De fleste populære nettlesere som Chrome, Edge tillater valg av tegnkoding gjennom innstillingene.

8. Konklusjon

I denne artikkelen diskuterte vi hvordan koding kan være et problem under programmering.

Vi diskuterte videre det grunnleggende, inkludert koding og tegnsett. Videre gikk vi gjennom forskjellige kodingsordninger og deres bruk.

Vi plukket også opp et eksempel på feil tegnkodingsbruk i Java og så hvordan vi skulle få det riktig. Til slutt diskuterte vi noen andre vanlige feilscenarier relatert til tegnkoding.

Som alltid er koden for eksemplene tilgjengelig på GitHub.